Skip to main content

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 + ")"
// ^^^^^^
info

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:

  1. Local scope, parent scope, then grandparent scope, and so on.
  2. Module scope.
  3. Implicits imported by name.
  4. Implicits imported by type.
  5. 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.