Implicit parameters
Overview
Section titled “Overview”Implicit parameters allow you to omit frequently-used function arguments at call sites when the compiler can infer them from context. This feature is particularly useful when working with ordered collections like Map and Set from the core library, which require comparison functions but where the comparison logic is usually obvious from the key type.
Other exampes are equal and toText functions.
Basic usage
Section titled “Basic usage”Declaring implicit parameters
Section titled “Declaring implicit parameters”When declaring a function, any function parameter can be declared implicit using the implicit type constructor:
For example, the core Map library, declares a function:
public func add<K, V>(self: Map<K, V>, compare : (implicit : (K, K) -> Order), key : K, value : V) { // ...}The implicit marker on the type of parameter compare indicates the call-site can omit it the compare argument, provided it can be inferred the call site.
A function can declare more than on implicit parameter, even of the same name.
func show<T, U>( self: (T, U), toTextT : (implicit : (toText : T -> Text)), toTextU : (implicit : (toText : U -> Text))) : Text { "(" # toTextT(self.0) # "," # toTextU(self.1) # ")"}In these cases, you can add an inner name to indicate the external names of the implicit parameters (both toText) and distinguish
them from the names used with the function body, toTextT and toTextU: these need to be distinct so that the body can call them.
The inner name (under implicit) overrides the local name of the parameter in the body.
Calling functions with implicit arguments
Section titled “Calling functions with implicit arguments”When calling a function with implicit parameters, you can omit the implicit arguments if the compiler can infer them:
import Map "mo:core/Map";import Nat "mo:core/Nat";
let map = Map.empty<Nat, Text>();
// Without implicits - must provide compare function explicitlyMap.add(map, Nat.compare, 5, "five");
// With implicits - compare function inferred from key typeMap.add(map, 5, "five");The compiler automatically finds an appropriate comparison function based on the type of the key argument.
The availabe candidates are:
- Any value named
comparewhose type matches the parameter type.
If there is no such value,
- Any field named
M.comparedeclared in some module availableM. - If there is more than one such field, none of which is more specific than all the others, the call is ambiguous.
An ambiguous call can always be disambiguated by supplying the explicit arguments for all implicit parameters.
Contextual dot notation
Section titled “Contextual dot notation”Implicit parameters dovetail nicely with the contextual dot notation. The dot notation and implicit arguments can be used in conjunction to shorten code.
For example, since the first parameter of Map.add is called self, we can both use map as the receiver of add “method” calls
and omit the tedious compare argument:
import Map "mo:core/Map";import Nat "mo:core/Nat";
let map = Map.empty<Nat, Text>();
// Using contextual dot notation, without implicits - must provide compare function explicitlymap.add(Nat.compare, 5, "five");
// Using contextual dot nation together with implicits - compare function inferred from key typemap.add(5, "five");Working with ordered collections
Section titled “Working with ordered collections”The primary use case for implicit arguments is simplifying code that uses maps and sets from the core library.
Map Example
Section titled “Map Example”import Map "mo:core/Map";import Nat "mo:core/Nat";
let inventory = Map.empty<Nat, Text>();
// Old style: explicitly pass Nat.compareMap.add(inventory, Nat.compare, 101, "Widget");Map.add(inventory, Nat.compare, 102, "Gadget");Map.add(inventory, Nat.compare, 103, "Doohickey");
let item1 = Map.get(inventory, Nat.compare, 102);
// With contextual dots and implicits: compare function inferredinventory.add(101, "Widget");inventory.add(102, "Gadget");inventory.add(103, "Doohickey");
let item2 = inventory.get(102);Set example
Section titled “Set example”The core Set type also takes advantage of implicit compare parameters.
import Set "mo:core/Set";import Text "mo:core/Text";
let tags = Set.empty<Text>();
// Old styleSet.add(tags, Text.compare, "urgent");Set.add(tags, Text.compare, "reviewed");let hasTag1 = Set.contains(tags, Text.compare, "urgent");
// With implicitstags.add("urgent");tags.add("reviewed");let hasTag2 = tags.contains("urgent");Building collections incrementally
Section titled “Building collections incrementally”Implicit arguments make imperative collection operations much cleaner:
import Map "mo:core/Map";import Text "mo:core/Text";
let scores = Map.empty<Text, Nat>();
// Add player scoresscores.add("Alice", 100);scores.add("Bob", 85);scores.add( "Charlie", 92);
// Update a scorescores.add("Bob", 95);
// Check and removeif (scores.containsKey("Alice")) { scores.remove("Alice");};
// Get sizelet playerCount = scores.size()How inference works
Section titled “How inference works”The compiler infers an implicit argument by:
- Examining the types of the explicit arguments provided.
- Looking for all candidate values for the implicit argument in the current scope that match the required type and name.
- From these, selecting the best unique candidate based on type specifity.
If there is no unique best candidate the compiler rejects the call as ambiguous.
If a callee takes several implicits parameter, either all implicit arguments must be omitted, or all explicit and implicit arguments must be provided at the call site, in their declared order.
Supported types
Section titled “Supported types”The core library provides comparison functions for common types:
Nat.compareforNatInt.compareforIntText.compareforTextChar.compareforCharBool.compareforBoolPrincipal.compareforPrincipal- etc.
Other implicit parameters declared by the core library are equals : (implicit : (T, T) -> Bool) and toText: (implicit : T -> Text).
Explicitly providing implicit arguments
Section titled “Explicitly providing implicit arguments”You can always provide implicit arguments explicitly when needed:
import Map "mo:core/Map";import Nat "mo:core/Nat";import {type Order} "mo:core/Order";
// Custom comparison function for reverse orderingfunc reverseCompare(a : Nat, b : Nat) : Order { Nat.compare(b, a)};
let reversedMap = Map.empty<Nat, Text>();// Explicitly provide the comparison functionreversedMap.add(reverseCompare, 5, "five");reversedMap.add(reverseCompare, 3, "three");This is useful when:
- Using custom comparison logic
- Working with custom types that have multiple possible orderings
- Improving code clarity in complex scenarios
Custom types
Section titled “Custom types”To use implicit arguments with your own custom types, define a comparison function:
import Map "mo:core/Map";import Text "mo:core/Text";import {type Order} "mo:core/Order";
type Person = { name : Text; age : Nat;};
module Person { public func compare(a : Person, b : Person) : Order { Text.compare(a.name, b.name) };};
// Now works with implicitslet directory = Map.empty<Person, Text>();directory.add({ name = "Alice"; age = 30 }, "alice@example.com");directory.add({ name = "Bob"; age = 25 }, "bob@example.com");
let email = directory.get({ name = "Alice"; age = 30 });Best practices
Section titled “Best practices”-
Use implicits for standard types: When working with
Nat,Text,Int,Principal, and other primitive types, let the compiler infer the comparison function. -
Be explicit with custom logic: When using non-standard comparison logic, explicitly provide the comparison function for clarity.
-
Name comparison functions consistently: Follow the convention of
ModuleName.compareto ensure proper inference. -
Consider readability: While implicits reduce boilerplate, explicit arguments may be clearer in some contexts, especially when teaching or documenting code.
-
Collections benefit most: The repeated operations on
MapandSetfromcoreparticularly benefit from implicit arguments since you call these functions frequently. -
Don’t go wild with implicit parameters. Use them sparingly.
Migration from explicit arguments
Section titled “Migration from explicit arguments”Existing code with explicit comparison functions will continue to work. You can adopt implicit arguments gradually:
import Map "mo:core/Map";import Nat "mo:core/Nat";
let data = Map.empty<Nat, Text>();
// Both styles work simultaneouslyMap.add(data, Nat.compare, 1, "one"); // ExplicitMap.add(data, 2, "two"); // ImplicitMap.add(data, 3, "three"); // ImplicitThere is no need to update existing code unless you want to take advantage of the cleaner syntax.
Performance considerations
Section titled “Performance considerations”Implicit arguments have no runtime overhead. The comparison function is resolved at compile time, so there is no performance difference between using implicit and explicit arguments. The resulting code is identical.