Wiki
Clone wikihelium / popl20 / Tutorial_Instances
Tutorial 3: Effect instances
Modularity of effects
One amazing feature of effect handlers is modularity. For example, we define the Choice
effect that consists of one operation, choose
, which takes a list of elements as its argument and returns a single element. We define a standard handler that interprets this signature as a nondeterministic choice and collects all possible results on a list:
signature Choice = | choose : forall {T : Type}, List T => T let rec concatMap f xs = match xs with | [] => [] | x :: xs' => f x @ concatMap f xs' end let hChoice = handler | choose xs => concatMap resume xs | return x => [x] end
Error
, already described in the previous section:
signature Error = | fail : forall {T : Type}, Unit => T let hErrorOpt = handler | fail _ => None | return x => Some x end let div x y = if y = 0 then fail () else x / y
e
that uses both Choice
and Error
. There are two natural ways to combine these two effects: one is when a single failure applies to the entire computation, killing off all nondeterministic branches, while in the other the failure is local, affecting only the current branch. We can achieve both scenarios simply by reordering handlers:
[IO,RE]> let e () = div 100 (choose [0,1] + choose [5,0]);; [IO,RE]> handle handle e () with hChoice with hErrorOpt;; None : Option (List Int) [IO,RE]> handle handle e () with hErrorOpt with hChoice;; [Some 20, None, Some 16, Some 100] : List (Option Int)
Effect instances
Modularity as in the example above is achieved easily, because each handler knows which effect it handles. But how one can use the same effect combined with itself? For example, we can define the "mutable memory cell" effect as follows:
signature State (S : Type) = | put : S => Unit | get : Unit => S let hState init = handler | put s => fn _ => resume () s | get _ => fn s => resume s s | return x => fn _ => x | finally f => f init end
`a
and `b
:
let fib n = handle `a in handle `b in let rec aux n = if n = 0 then get `a () else (put `b (get `a () + get `b ()); put `a (get `b () - get `a ()); aux (n - 1)) in aux n with hState 1 with hState 0
handle `a in e with h
means that in the expression e
we can decorate the operations of the effect with instance annotation `a
. Think of `a
as a variable bound in e
by the handler. Since instance variables cannot mix with other values, there is a separate syntax for them: they are identifiers preceded by a backtick `
.
Instance abstraction
Since instance variables are bound by handlers, how can we write a function that invokes some effects, but does not handle them? The answer is: take the instances as arguments. Since we have a separate syntax for them, they won't be confused with the usual arguments.
let swap `a `b () = let c = get `a () in (put `a (get `b ()); put `b c)
swap
:
[IO,RE]> swap;; <func> : forall (`a : State 't) (`b : State 't), Unit ->[`a,`b] Unit
swap
takes two instances, both of the effect State 't
for a type 't
. The annotation [`a,`b]
in the type Unit ->[`a,`b] Unit
means that the type-and-effect system of Helium discovered that when applied to an argument, the function swap
may invoke effects on the instances `a
and `b
. We can use swap
simply giving it instances as arguments:
let foo () = handle `a in handle `b in handle `c in swap `a `b (); (get `a (), get `b (), get `c ()) with hState 1 with hState 2 with hState 3
Automatic instance abstraction and application
It is often the case that we do not want to bother with instances. Consider, for example, the following function that manipulates state:
let update f = put (f (get ()))
update
is concerned only with one instance of state. In such situations, Helium will automatically add appropriate instance abstractions and applications, so that the function above is equivalent to
let update_explicit `a f = put `a (f (get `a ()))
[IO,RE]> update;; <func> : forall (`a : State 't), ('t ->[`a,'e] 't) ->[`a,'e] Unit
forall
quantification over instances.
The same applies to handlers. If there is only one handler for a particular effect in the scope, it is Ok not to mention the instances at all. For example, the code
handle put 2; update (fn x => x*x); printInt (get ()) with hState 0
handle `a in put `a 2; update `a (fn x => x*x); printInt (get `a ()) with hState 0
put
, get
, and update
are called. In particular, the following does not type-check:
handle `a in handle `b in put 2 with hState 0 with hState 0
put
refers to the instance `a
or `b
. The error message says so:
[IO,RE]> handle `a in handle `b in put 2 with hState 0 with hState 0;; repl:3:3-5: error: Ambiguous instance of signature State 't. Try applying it explicitly.
Updated