Implicits
Implicits are values that the compiler can automatically insert into functions. These enable a number of useful features, such as polymorphism and context-sensitive behaviors.
Implicit arguments to functions
Implicit arguments to functions are wrapped in &[...]
. For example,
add_many: &[Add a] -> a -> a -> a -> a
add_many x y z = x + y + z
The add_many
function can add any three values together for which there is an
Add
implicit, such as Int
and Float
. Note that the Add a
value is not
explicitly used in the function; however, it is implicitly passed along to the
+
operators.
To use the Add a
value in the function, it can be added to the argument list
wrapped in &[...]
.
add_many: &[Add a] -> a -> a -> a -> a
add_many &[adder] x y z = adder#add (adder#add x y) z
If there is more than one implicit argument, then they can be filled left to right.
mul_add: &[Mul a] -> &[Add a] -> a -> a -> a -> a
mul_add &[multiplier] &[adder] x y z = adder#add (multiplier#mul x y) z
To capture just the second implicit argument, the first must be skipped.
mul_add: &[Mul a] -> &[Add a] -> a -> a -> a -> a
mul_add &[_] &[adder] x y z = adder#add (x * y) z
An implicit argument can be explicitly passed to a function by wrapping it in
&[...]
. Note that these are passed left to right, and in order to skip
passing arguments that are taken from context (as above), simply pass &[_]
,
which will instruct the compiler to fill the implicit argument based on
context.
Implicit types
A type can be marked as implicit so that any values of that type are resolved as implicit arguments.
import std/assert
#[implicit]
type Wrap = Wrap (String -> String)
// This value will be used for implicit resolution.
square_wrap: Wrap
square_wrap = Wrap \s -> "[" + s + "]"
wrap: &[Wrap] -> String -> String
wrap &[w] s = w#0 s
main: () -> ()
main = assert.eq (wrap "abc") "[abc]"
Implicit values
A single value can be marked as implicit, regardless of whether the type is implicit.
#[implicit]
curly_wrap: Wrap
curly_wrap = Wrap \s -> "&[" + s + "]"
Additionally, within a function, values declared with use
are available for
implicit resolution.
main: () -> ()
main () = do
// This shadows implicits in the module scope.
use round_wrap = Wrap \s -> "(" + s + ")"
assert.eq (wrap "abc") "(abc)"
Type inference for use
values is tricky, as the compiler will not attempt to
infer the type based on where it could be inserted. The type can be omitted if
the right-hand side is unambiguous in type; however, a type annotation can be
provided if necessary.
// The type annotation is not necessary here.
use squiggle_wrap: Wrap = Wrap \s -> "(" + s + ")"
// ^^^^^^
Unlike let bindings, a use
binding does not bind
to a pattern. A use
binding requires a single name to bind.
This is to avoid confusion on what is put into implicit context. For example,
does use (a, b) = ...
put the value of a
, the value of b
, or the entire
tuple into implicit context? By disallowing patterns in use
bindings, this
confusion never arises.
Higher-order functions with implicits
A higher-order function (i.e., a function passed as an argument to another function) can also use implicit arguments. In this case, the implicit context comes from where the function is applied, not where it is passed.
import std/assert
import std/string
type Indent = Indent Int
format: String -> (&[Indent] -> String -> String) -> String
format s formatter = formatter &[Indent 4] s
double_indent_formatter: &[Indent] -> String -> String
double_indent_formatter &[indent] s = str.repeat " " (2 * indent#0) + s
main: () -> ()
main () = assert.eq (format "abc" double_indent_formatter) " abc"
// -·-·-·-·
Constraints
Since an implicit may itself use other implicits, it is possible to add constraints by requiring that other implicits exist.
import std/assert
type Pair a b = Pair a b
// `Default a` and `Default b` are constraints on the
// `Default (Pair a b)` implicit.
default_pair: &[Default a] -> &[Default b] -> Default (Pair a b)
default_pair = Pair default default
main: () -> ()
main () = do
// The implicit of `Default (Pair a b)` above is automatically
// inserted into the `default` function call. The implicit will pull
// in `Default Int` and `Default String` implicits so that defaults
// for the `Int` and `String` in the `Pair` can be computed.
let pair: Pair Int String = default
assert.eq pair (Pair 0 "")
Implicit search locations
Implicit arguments are resolved by finding the nearest and then the most specific value. The compiler searches in order:
- Local scope, parent scope, then grandparent scope, and so on.
- Module scope.
- Implicits imported by name.
- Implicits imported by type.
- Prelude inserted by the compiler.
If the currently searched scope is function-level, then all implicit arguments to that function are included in the search. In all scopes except the module-level scope, the most tightly scoped implicits take precedence (i.e., implicits overwrite earlier ones in the case of conflict).
The search stops as soon as at least one compatible implicit is found in a
level. If exactly one is found, it is used. If two or more are found, the most
specific one is used (e.g., Add Int
is more specific than Add a
). If there
is a conflict where the leading candidates are of equal specificity, then a
compile-time error is issued.