Wiki
Clone wikihelium / popl20 / Tutorial_EffectsAndHandlers
Tutorial 2: Effects and Handlers
The key feature of Helium is an approach to control operators that is based on algebraic effects and their handlers. In the following, we briefly introduce these features from a programmer's perspective.
Effect signatures and operations
In Helium, effects are a special brand of types that track what computational effects can be performed by a given computation. They are declared locally and available only within their scope, and the type system makes sure that an effect cannot escape its scope. New effects can be introduced by declaring their signature. For example, we can declare a simple effect, Error
, as follows:
signature Error (A : Type) = | fail : forall {T : Type}, A => T
Error
effect has one operation called fail
, which we can use as any other identifier. This operation has the following polymorphic type:
fail : forall (`a : Error 't) {'s : Type}, 't ->[`a] 's
fail
to any argument and use the resulting expression in any context, but the resulting computation will have an effect Error a
associated with it (possibly, among other effects). The first universally quantified varible `a
says that this operation works for any instance of the effect Error
. Instances are discussed in the next section, we can safely ignore them for now. The type of fail
means that we cannot simply use fail
in the REPL, as the only effects the REPL expects is IO
and RE
(runtime error):
[IO,RE]> fail 5;; repl:1:1-4: error: There is no implicit scope which can bind instance of signature Error 'a.
Effect handlers
How, then, can we call a computation that performs an effect (other than IO
or RE
) from the REPL? We have to provide a handler. Note that we didn't give a definition of what the fail
operation does — it is simply a "constructor" of the Error
effect. The interpretation of this constructor is a job for a handler. For example, given a computation that may use the fail
operation, we could wrap its result in the usual Option
datatype, returning Some
if the result was computed successfully, and None
if we encounter an error. The definition of such a handler in Helium looks as follows:
let hErrorOpt = handler | fail _ => None | return n => Some n end
[IO,RE]> handle 2 + 2 with hErrorOpt;; Some 4 : Option Int [IO,RE]> handle 2 + fail () with hErrorOpt;; None : Option Int
Resuming the computation
The particularly appealing aspect of effect handlers is the they can resume the computation after encountering an operation. For example, let us look at another simple effect, Reader
:
signature Reader (A : Type) = | ask : Unit => A
Reader
effect:
let c () = ask () * ask () + 17
ask
operations with an answer — say, 5:
[IO, RE]> handle c () with | ask () => resume 5 end;; 42 : Int
let with5 c = handle c with | ask () / r => r 5 end
Updated