-
Algebraic Effect Handlers with Resources and
DeepFinalization.Microsoft Technical Report, MSR-TR-2018-10,
v1.
DAAN LEIJEN,Microsoft Research
Algebraic effect handlers are a powerful abstraction mechanism
that can express many complex control-flowmechanisms. In this
article we first define a basic operational semantics and type
system for algebraic effecthandlers, and then build on that to
formalize various optimizations and extensions. In particular, we
show howto optimize tail-resumptive operations using skip frames,
formalize a semantics and type rules for first-classresource
handlers that can express polymorphic references, and formalize a
design for finalizers and initializersto handle external resources
with linearity constraints. We introduce the concept of deep
finalization whichensures finalization is robust across operation
handlers even if operations themselves do not resume.
updates:v1, 2018-04-03: Initial version.
1. INTRODUCTIONAlgebraic effects [36] and their extension with
handlers [37, 38], are a novel way to describe manycontrol-flow
mechanisms in programming languages. In general any free monad can
be expressedas an effect handler and they have been used to
describe complex control structures such as iterators,async-await,
concurrency, parsers, state, exceptions, etc. (without needing to
extend the compileror language) [10, 14, 16, 26, 48].Recently,
there are various implementations of algebraic effects, either
embedded in other
languages like Haskell [16, 48], Scala [5], or C [27], or built
into a language, like Eff [2], Links [14],Frank [32], Koka [28],
and Multi-core OCaml [10, 46]. Even though the theoretical
foundationsfor algebraic effects and handlers are well-understood,
for any practical implementation of effecthandlers there are many
open questions, like the potential efficiency of handlers, or their
interactionwith linear resources. In this paper we try to address
some of these questions by giving an end-to-enddescription and
formalization of effect handlers and various novel extensions.In
Section 4 we define the operational semantics of basic algebraic
effects and handlers based
on [28]. We build on these semantics as the foundation for
various optimizations and extensionsthat are required when using
effects in practice:• Section 5 describes skip frames and
formalizes an important optimization that avoids captur-ing and
restoring the stack for tail-resumptive operations, and essentially
turns them intodynamic method calls. Since almost all operations in
practice are tail-resumptive, this is avery important optimization
for any practical implementation. We prove that the extendedformal
semantics with the tail-resumptive optimization is sound with
respect to the originalsemantics.• Section 6 extends the semantics
with effect injection allowing to skip innermost handlers.
Bier-nacki et al. [3] show that this is an essential operation for
writing composable higher-orderfunctions that use effect handlers.
Their formulation used a separate 0-free predicate, but
wereformulate this using straightforward evaluation contexts, and
we prove that these conceptsare equivalent.• Our main contribution
is in Section 7 where we extend the semantics further with
handlersthat are can be addressed as first-class values, called
resources. This adds a lot more expres-siveness but surprisingly
only needs minimal changes to the original semantics. As
such,resources are a powerful abstraction but can still be
understood and reasoned about as regular
1
-
effect handlers. We show through various examples how resources
can be used to model files,polymorphic references, and event
streams in CorrL [4].• When using external resources, like files,
we need a robust way to run finalization code.However, we cannot
specialize this for just exception effects but need a general
mechanismthat works for any effect definition: any operation might
not resume, or resume more thanonce. Both those cases cause
problems when working with an external resource that hasa linearity
constraint. Section 8 extends the semantics further with general
finalizers andinitializers that work across any effect and can
safely encapsulate linear resources. We alsointroduce the novel
concept of deep finalization (Section 8.2) to write robust
operation clauses.
Except for initializers1, all of the above contributions have
been fully implemented [25]. We firstgive a mini-overview of
algebraic effects in Section 2, and formalize the syntax and type
system inSection 3. After that we describe the basic semantics and
discuss the various extensions. There isno separate related work
section as we try to discuss related work in each section
inline.
2. BASIC ALGEBRAIC EFFECTSWe are demonstrate all our examples in
the context of the Koka programming language as this isone of the
few implementations with a full type inference system that tracks
effects of each function.The type of a function has the form τ→ ϵ τ
′ signifying a function that takes an argument of typeτ , returns a
result of type τ ′ and may have a side effect ϵ . We can leave out
the effect and writeτ→ τ ′ as a shorthand for the total function
without any side effect: τ→ ⟨⟩ τ ′. A key observationon Moggi’s
early work on monads [34] was that values and computations should
be assigned adifferent type. Koka applies that principle where
effect types only occur on function types; and anyother type, like
int, truly designates an evaluated value that cannot have any
effect.
Koka has many features found in languages like ML and Haskell,
such as type inference, algebraicdata types and pattern matching,
higher-order functions, impredicative polymorphism, open datatypes,
etc. The pioneering feature of Koka is the use of row types with
scoped labels to track effectsin the type system. This gives Koka a
very strong semantic foundation combining the purity ofHaskell with
the call-by-value semantics of ML.
There are various ways to understand algebraic effects and
handlers. As described originally [36,38], the signature of the
effect operations forms a free algebra which gives rise to a free
monad.Free monads provide a natural way to give semantics to
effects, where handlers describe a fold overthe algebra of
operations [45]. Using a more operational perspective, we can also
view algebraiceffects as resumable exceptions. We therefore start
our overview by modeling exceptional controlflow.
2.1. Exceptions as Algebraic EffectsThe exception effect exc can
be defined in Koka as:effect exc {
raise( s : string ) : a
}
This defines a new effect type excwith a single primitive
operation, raisewith type string → exc afor any a (Koka uses single
letters for polymorphic type variables). The raise operation can be
usedjust like any other function:
1Initializers are already supported by the runtime but have no
builtin syntax yet.
2
-
fun safediv( x, y ) {
if (y==0) then raise("divide by zero") else x / y
}
Type inference will infer the type (int,int) → exc int
propagating the exception effect. Upto this point we have
introduced the new effect type and the operation interface, but we
havenot yet defined what these operations mean. The semantics of an
operation is given through analgebraic effect handler which allows
us to discharge the effect type. The standard way to
dischargeexceptions is by catching them, and we can write this
using effect handlers as:fun catch(action,h) {
handle(action) {
raise(s) → h(s)}
}
The handle construct for an effect takes an action to evaluate
and a set of operation clauses. Theinferred type of catch is:catch
: ( action: () → ⟨exc|e⟩ a, h : string → e a) → e aThe type is
polymorphic in the result type a and its final effects e, where the
action argument hasan effect ⟨exc|e⟩: this syntax denotes effect
row extension, and states that action can have an exceffect and
possibly more effects e. As we can see, the handle construct
discharged the exc effectand the final result effect is just e. For
example,fun zerodiv(x,y) {
catch( { safediv(x,y) }, fun(s){ 0 } )
}
has type (int,int) → ⟨ ⟩ int and is a total function. Note that
the Koka braced syntax { safediv(x,y) }denotes an anonymous
function that takes no arguments, i.e. it is equivalent to fun(){
safediv(x,y) }.
Besides clauses for each operation, each handler can have a
return clause too: this is applied tothe final result of the
handled action. In the previous example, we just passed the result
unchanged,but in general we may want to apply some transformation.
For example, transforming exceptionalcomputations into maybe
values:fun to-maybe(action) {
handle(action) {
return x → Just(x)raise(s) → Nothing
}}
with the inferred type (() → ⟨exc|e⟩ a) → e maybe⟨a⟩. Note that
Koka uses angled brackets forboth effect row types, but also for
type application as in the result type maybe⟨a⟩.
The handle construct is actually syntactic sugar over the more
primitive handler construct:handle(action) { ... } ≡ (handler{ ...
})(action)A handler just takes a set of operation clauses for an
effect, and returns a function that dischargesthe effect over a
given action. This allows us to express to-maybe more concisely as
a (function)value:val to-maybe = handler {
return x → Just(x)raise(s) → Nothing
}
3
-
with the same type as before.Just like monadic programming,
algebraic effects allows us to conveniently program with excep-
tions without having to explicitly plumb maybe values around.
When using monads though we haveto provide a Monad instance with a
bind and return, and we need to create a separate
dischargefunction. In contrast, with algebraic effects we only
define the operation interface and the dischargeis implicit in the
handler definition.
2.2. State: Resuming OperationsThe exception effect is somewhat
special as it never resumes: any operations following the raiseare
never executed. Usually, operations will resume with a specific
result instead of cutting thecomputation short. For example, we can
have an input effect:effect input {
getstr() : string
}
where the operation getstr returns some input. We can use this
as:fun hello() {
val name = getstr()
println("Hello " + name)
}
An obvious implementation of getstr gets the input from the
user, but we can just as well create ahandler that takes a set of
strings to provide as input, or always returns the same string:val
always-there = handler {
return x → xgetstr() → resume("there")
}
Every operation clause in a handler brings an identifier resume
in scope which takes as an argumentthe result of the operation and
resumes the program at the invocation of the operation – if
theresume occurs at the tail position (as in our example) it is
much like a regular function call. Executingalways-there(hello)
will output:> always-there(hello)
Hello there
As another example, we can define a stateful effect:effect
state⟨s⟩ {
get() : s
put( x : s ) : ()
}
The state effect is polymorphic over the values s it stores. For
example, infun counter() {
val i = get()
if (i ≤ 0) then () else {println("hi")
put(i - 1);
counter()
}
}
4
-
Expressions e ::= e(e) application| val x = e; e binding|
handlelh(e) handler| v value
Values v ::= x | C variables, constants| λx . e lambda
expressions| opl operation of effect l
Clauses h ::= return x → e return clause| op(x) → e; h operation
clause
Static effect label l ::= l (for now equal to effect
constants)
Fig. 1. Syntax of expressions
the type becomes () → ⟨state⟨int⟩,console,div|e⟩ () with the
state instantiated to int. Todefine the state effect we could use
the built-in state effect of Koka, but a cleaner way is to
useparameterized handlers. Such handlers take a parameter that is
updated at every resume. Here is apossible definition for handling
state:val state = handler(s) {
return x → (x,s)get() → resume(s,s)put(s’) → resume((),s’)
}
We see that the handler binds a parameter s (of the polymorphic
type s), the current state. The returnclause returns the final
result tupled with the final state. The resume function in a
parameterizedhandler takes now multiple arguments: the first
argument is the result of the operation, while thelast argument is
the new handler parameter used when handling the resumption. The
get operationleaves the current state unchanged, while the put
operation resumes with the passed-in stateargument as the new
handler parameter. The function returned by the handler construct
now takesthe initial state as an extra argument:state : (init: s,
action: () → ⟨state⟨s⟩|e⟩ a) → e (a,s)and we can use it as:>
state(2,counter)
hi
hi
This concludes the overview but we covered many essential
aspects algebraic effects. We refer toother work for more in-depth
explanations and examples [2, 14, 16, 29, 48].
3. SYNTAX AND TYPES
3.1. Type RulesIn this section we give a formal definition of
our polymorphic row-based effect system for the corecalculus of
Koka. The material in this section (and large parts of the next
section on semantics) has
5
-
Types τ k ::= αk type variable| ck ⟨τ k11 , . . .,τ
knn ⟩ kind of c is (k1, . . ., kn) → k
Kinds k ::= ∗ | e values, effects| k effect constants| (k1, . .
., kn) → k type constructor
Type scheme σ ::= ∀αk . σ | τ ∗
Type constants (), bool :: ∗ unit, booleans(_→ _ _) :: (∗, e, ∗)
→ ∗ functions⟨⟩ :: e empty effect⟨_ | _⟩ :: (k, e) → e effect
extensionexn, . . . :: k various effect constants
Value types τ � τ ∗ kinds are ∗ by defaultTotal functions τ1→ τ2
� τ1→ ⟨⟩ τ2
Effect labels l � ckEffect type ι � l⟨τ1, . . .,τn⟩
Effect rows ϵ � τ eEffect row variables µ � αeClosed effect rows
⟨ι1, . . ., ιn⟩ � ⟨ι1, . . ., ιn | ⟨⟩ ⟩Effect row extension ⟨ι1, .
. ., ιn | ϵ⟩ � ⟨ι1 | . . . ⟨ιn | ϵ⟩ . . . ⟩
Fig. 2. Syntax of types and kinds
ϵ � ϵ [eq-refl]ϵ1 � ϵ2
⟨ι | ϵ1⟩ � ⟨ι | ϵ2⟩[eq-head]
ϵ1 � ϵ2 ϵ2 � ϵ3
ϵ1 � ϵ3[eq-trans]
ι1 . ι2
⟨ι1 | ⟨ι2 | ϵ⟩ ⟩ � ⟨ι2 | ⟨ι1 | ϵ⟩ ⟩[eq-commute]
l , l′
l⟨τ1, . . .,τn⟩ . l′⟨τ ′1, . . .,τ ′n⟩[uneq-label]
Fig. 3. Row equivalence
been presented before in a similar form [28] but we include it
again here to make this article selfsufficient. The well informed
reader may skip through these sections.
Figure 2 defines the syntax of types and expressions. The
expression grammar is straightforwardbut we are careful to
distinguish values v from expressions e that can have effects.
Values consistof variables x, constants C, operations op, and
lambda’s. Expressions include handler expressionshandlelh(e) where
h is a set of operation clauses and l identifies the effect that is
handled in theexpression ϵ . The handler construct of the previous
section can be seen as syntactic sugar, where:
handlerlh ≡ λf . handlelh(f ())
6
-
Γ(x) = σΓ ⊢ x : σ | ϵ
[Var]
Γ, x : τ1 ⊢ e : τ2 | ϵ ′
Γ ⊢ λx . e : τ1→ ϵ ′ τ2 | ϵ[Lam]
Γ ⊢ e : τ | ⟨⟩ α ̸∈ ftv(Γ)Γ ⊢ e : ∀α . τ | ϵ [Gen]
Γ ⊢ e1 : σ | ϵ Γ, x : σ ⊢ e2 : τ | ϵΓ ⊢ val x = e1; e2 : τ |
ϵ
[Let]
Γ ⊢ e1 : τ2→ ϵ τ | ϵ Γ ⊢ e2 : τ2 | ϵΓ ⊢ e1(e2) : τ | ϵ
[App]
Γ ⊢ e : ∀α . τ | ϵΓ ⊢ e : τ [α 7→ τ ] | ϵ
[Inst]
Γ ⊢ e : τ | ⟨ι | ϵ⟩ S(l) = {op1, . . ., opn}Γ, x : τ ⊢ er : τr |
ϵ Γ ⊢ opi : τi → ⟨ι⟩ τ ′i | ⟨⟩Γ, resume : τ ′i → ϵ τr , xi : τi ⊢
ei : τr | ϵ
Γ ⊢ handlel{ op1(x1) → e1; . . .; opn(xn) → en; return x → er
}(e) : τr | ϵ[Handle]
Fig. 4. Type rules.
For simplicity we assume that all operations take just one
argument. We also use membershipnotation op(x) → e ∈ h to denote
that h contains a particular operation clause. Sometimes weshorten
this to op ∈ h.Well-formed types are guaranteed through kinds k
which we denote using a superscript, as in
τ k . We have the usual kinds for value types ∗ and type
constructors→, but because we use a rowbased effect system, we also
have kinds for effect rows ϵ , and effect constants l. When the
kind ofa type is immediately apparent or not relevant, we usually
leave it out. For clarity, we use α forregular type variables, and
µ for effect type variables.
Effect types are defined as a row of effect label types ι. Such
row is either empty ⟨⟩, a polymorphiceffect variable µ, or an
extension of an effect ϵ with a label ι, written as ⟨ι | ϵ⟩. Effect
labels muststart with an effect constant l and are never
polymorphic. By construction, effect types are either aclosed
effect of the form ⟨ι1, . . ., ιn⟩, or an open effect of the form
⟨ι1, . . ., ιn | µ⟩.We cannot use direct equality on types since we
would like to regard effect rows equivalent
up to the order of their effect constants. Figure 3 defines an
equivalence relation ( � ) betweeneffect rows. This relation is
essentially the same as for the scoped labels record system [23]
withthe difference that we ignore the type arguments when comparing
labels. This is apparent in theUneq-Label rule. This is essential
to guarantee a deterministic and terminating unification
algorithmwhich is essential for type inference.
In contrast to other record calculi [13, 31, 39, 44], our
approach does not require extra constraints,like lacks or absence
constraints, on the types which simplifies the type system
significantly. Thesystem also allows duplicate labels, where an
effect ⟨exc, exc⟩ is legal and different from ⟨exc⟩. Thisproves to
be an essential feature to give a proper type to effect injection
(Section 6), and also toemulate generative types where we need
types of the form ⟨heap⟨s1,α⟩, heap⟨s2, β⟩|ϵ⟩ (Section 6.2).
3.2. Type InferenceThe type rules for our calculus are given in
Figure 4. A type environment Γ maps variables to typesand can be
extended using a comma: if Γ′ equals Γ, x : σ , then Γ′(x) = σ and
Γ′(y) = Γ(y) forany x , y. A type rule Γ ⊢ e : τ | ϵ states that
under environment Γ, the expression e has typeτ with possible
effects ϵ .
The type rules are quite standard. The rule var derives the type
of a variable x with an arbitraryeffect ϵ . We may have expected to
derive only the total effect ⟨⟩ since the evaluation of a variable
has
7
-
no effect at all. However, there is no rule that lets one
upgrade the final effect and instead we needto pick the final
effect right away. Another way to look at this is that since the
variable evaluationhas no effect, we are free to assume any
arbitrary effect. We use the Var rule too for operations opand
constants c where we assume the types of those are part of the
initial environment.The lam rule is similar in that it assumes any
effect ϵ for the result since the evaluation of a
lambda is a value. At this rule, we also see how the effect
derived for the body of a lambda ϵ ′ shiftsto the derived function
type τ1 → ϵ ′ τ2. Rule app is standard and derives an effect ϵ
requiring thatits premises derive the same effect as the function
effect.Rules inst and gen instantiate and generalize types. The
generalization rule has an interesting
twist as it requires the derived effect to be total. When
combining our calculus with polymorphicmutable reference cells,
this is required to ensure a sound semantics. This is the semantic
equivalentto the syntactic value restriction in ML. In our core
calculus we cannot define polymorphic referencecells directly so
the restriction is not necessary persé but it seems good taste to
leave it in as it isrequired for the full Koka language.Finally,
the handle rule types effect handlers. We assume that effect
declarations populate an
initial environment Γ0 with the types of declared operations,
and also a signature environment Sthat maps declared effect labels
to the set of operations that belong to it. We also assume that
alloperations have unique names, such that given the operation
names, we can uniquely determine towhich effect l they belong.
The rule handle requires that all operations in the signature
S(l) are part of the handler, andwe reject handlers that do not
handle all operations that are part of the effect l. The return
clauseis typed with x : τ where τ is the result type of the handled
action. All clauses must have thesame result type τr and effect ϵ .
For each operation clause opi(xi) → ei we first look up the typeof
opi in the environment as τi → ⟨l⟩ τ ′i , and bind xi to the
argument type of the operations, andbind resume to the function τ
′i → τr where the argument type of resume is the result type of
theoperation. The derived type of the handler is a function that
discharges the effect type l.
As described in earlier work [24], type inference is
straightforward based on Hindley-Milner [15,33], and is sound and
complete with respect to the declarative type rules.
3.3. Simplifying TypesThe rule app is a little surprising since
it requires both the effects of the function and the argumentsto
match. This only works because we set things up to always be able
to infer the effects of functionsthat are ‘open’ – i.e. have a
polymorphic µ in their tail. For example, consider the identity
function:
id = λx . x
If we assign the valid type ∀α . α → ⟨⟩ α to the id function, we
get into trouble quickly. Forexample, the application
id(raise(”hi”)) would not type check since the effect of id is
total whilethe effect of the argument contains exc. Of course, the
type inference algorithm always infers amost general type for id,
namely ∀α µ . α → µ α which has no such problems.
In practice though we wish to simplify the types more and leave
out ‘obvious’ polymorphism. InKoka we adopted two extra type rules
to achieve this. The first rule opens closed effects of
functiontypes:
Γ ⊢ e : τ1→ ⟨l1, . . ., ln⟩ τ2 | ϵΓ ⊢ e : τ1→ ⟨l1, . . ., ln | ϵ
′⟩ τ2 | ϵ
[open]
8
-
Evaluation Contexts:
E ::= □ | E(e) | v(E) | val x = E; e | handlelh(E)
Hl ::= □ | Hl(e) | v(Hl) | val x = Hl ; e| handlel′h (Hl) if l ,
l′
Reduction Rules:
(δ ) C(v) −→ δ (C, v) if δ (c, v) is defined(β) (λx . e)(v) −→
e[x 7→ v](let) val x = v; e −→ e[x 7→ v]
(return) handlelh(v) −→ e[x 7→ v]with(return x → e) ∈ h
(handle) handlelh(Hl[opl(v)]) −→ e[x 7→ v, resume 7→
r)]with(op(x) → e) ∈ hr = λy. handlelh(Hl[y])
Fig. 5. Reduction rules and evaluation contexts
With this rule, we can type the application id(raise(”hi”)) even
with the simpler type assigned toid as we can open the effect type
of id using the open rule to match the effect of raise(”hi”).
Wecombine this with a closing rule (which is just an instance of
inst/gen):
Γ ⊢ e : ∀µα . τ1→ ⟨l1, . . ., ln | µ⟩ τ2 | ϵµ ̸∈ ftv(τ1,τ2, l1,
. . ., ln)Γ ⊢ e : ∀α . τ1→ ⟨l1, . . ., ln⟩ τ2 | ϵ [close]
During inference, the rule close is applied (when possible)
before assigning a type to a let-boundvariable. In general, such
technique would lead to incompleteness where some programs that
werewell-typed before, may now be rejected since close assigns a
less general type. However, due toopen this is not the case: at
every occurrence of such let-bound variable the rule open can
alwaysbe applied (possibly surrounded by inst/gen) to lead to the
original most general type – i.e. eventhough the types are
simplified, the set of typeable programs is unchanged.
4. SEMANTICSIn this section we define a precise operational
semantics. Even though algebraic effects are originallyconceived
with a semantics in category theory, we will give a more regular
operational semanticsthat corresponds closely to various
implementations in practice.
The operational semantics is given in Figure 5 and consists of
just five evaluation rules. We usetwo evaluation contexts: the E
context is the usual one for a call-by-value lambda calculus. TheHl
context is used for handlers. In particular, it evaluates down
through any handlers that do not
9
-
handle the effect l. This is used to express concisely that the
innermost handler handles a particularoperation.
The first three reduction rules, (δ ), (β), and (let) are the
standard rules of call-by-value evaluation.The final two rules
evaluate handlers. Rule (return) applies the return clause of a
handler when theargument is fully evaluated. Note that this
evaluation rule subsumes both lambda- and let-bindingsand we can
define both as a reduction to a handler without any operations:
(λx . e1)(e2) ≡ handle⟨⟩{return x → e1}(e2)
and
val x = e1; e2 ≡ handle⟨⟩{return x → e2}(e1)
This equivalence is used in the Frank language [32] to define
everything, including functions andapplications, as handlers.The
next rule, (handle), is where all the action is. Here we see how
algebraic effect handlers
are closely related to delimited continuations as the evaluation
rules captures a delimited ‘stack’Hl[opl(v)] under the handler h.
Using a Hl context ensures by construction that only the
innermosthandler for an effect l will handle the operation opl(v).
Evaluation continues with the expression ϵbut besides binding the
parameter x to v, also the resume variable is bound to the
continuation:λy. handlelh(Hl[y]). Applying resume results in
continuing evaluation at Hl with the suppliedargument as the
result. Moreover, the continued evaluation occurs again under the
handler h.
Resuming under the same handler is important as it ensures that
our semantics correspond to theoriginal categorical interpretation
of algebraic effect handlers as a fold over the effect algebra
[38].If the continuation is not resumed under the same handler, it
behaves more like a case statementdoing only one level of the fold.
Such handlers are sometimes called shallow handlers [16, 32].
Inthat case resume would only restore the stack without the
handler: resume 7→ λy. Hl[y].For simplicity, we ignore
parameterized handlers for now but we come back to it in Section
8
when formalizing initializers. The reduction rule is
straightforward though – a handler with asingle parameter p is
reduced as:
handlelh(p = vp)(Hl[opl(v)])−→ { op(v) → e ∈ h }
e[x 7→ v, p 7→ vp, resume 7→ λy q. handlelh(p = q)(Hl[y])]
where it is apparent how resume now resumes under a handler with
an updated parameter p.Using the reduction rules of Figure 5 we can
define the overall evaluation function (7−→), where
E[e] 7−→ E[e′] iff e −→ e′. We also define the function 7−→∗ as
the reflexive and transitive closureof 7−→.
4.1. Dot NotationWe can view the evaluation contexts E and Hl ,
as evaluation stacks, where handlelh are specialframes on the
stack. To make this notion of an evaluation context as a stack more
apparent, weoften use dot notation where we use the following
shorthands:Hl · e � Hl[e]E · e � E[e]e1 · e2 � e1(e2)This makes
most proofs and reduction rule easier to read and follow.
10
-
4.2. Return as an OperationInstead of having a separate rule for
"return x" clauses, we can treat them as syntactic sugar for
animplicit return(x) operation, where we consider:
handlel{h; return x → er }(e)
as a shorthand for:
handlel{h; return(x) → er }(return(e))
where resume ̸∈ fv(er ) (and return ̸∈ op(h)). By treating
return clauses as operations, we captureall substitution under a
single rule, and don’t require extra rules for parameterized
handlers forexample (where a return clause can access the local
parameter as well).
We can imagine adding the simpler (value) rule to the
reductions:(value) handlelh(v) −→ vbut that it is not strictly
necessary. Since we assume that every handler has a "return x“
clause (whichis by default ”return x → x"), all handlers always
eventually end with the return(v) operation andthe plain value case
never occurs.We can show that the new syntactic transformation
leads to equivalent reductions as with the
original rule.
Proof. Starting from the premise of the original (return) rule,
we have:
handlel{h; return x → er }(v){ { syntax }
handlel{h; return(x) → er }(return(v))−→
er [x 7→ v, resume 7→ r]= { resume ̸∈ fv(er ) }
er [x 7→ v]
which is equivalent to the original rule. □
4.3. Comparison with Delimited ContinuationsShan [41] has shown
that various variants of delimited continuations can be defined in
terms ofeach other. Following Kammar et al. [16], we can define a
variant of Danvy and Filinski’s [8] shiftand reset operators,
called shift0 and reset0, as
reset0(X[shift0(λk. e)]) −→ e[k 7→ λx . reset0(X[x])]
where we write X for a context that does not contain a reset0.
Therefore, the shift0 captures thecontinuation up to the nearest
enclosing reset0. Just like handlers, the captured continuation
isitself also wrapped in a reset0. Unlike handlers though, the
handling is done by the shift0 directlyinstead of being done by the
delimiter reset0. From the reduction rule, we can easily see that
wecan implement delimited continuations using algebraic effect
handlers, where shift0 is an operationin the delim effect and X ≡
Hdelim:
reset0(e) � handledelim{ shift0(f ) → f (resume) }(e)
11
-
Using this definition, we can show it is equivalent to the
original reduction rule for delimitedcontinuations, where we write
h for the handler shift0(f ) → f (resume):
reset0(X[shift0(λk. e)])�
handledelimh · Hdelim · shiftdelim0 (λk. e)
−→(f (resume))[f 7→ λk. e, resume 7→ λx . handledelimh · Hdelim
· x]−→(λk. e)(λx . handledelimh · Hdelim · x)−→
e[k 7→ λx . handledelimh · Hdelim · x]�
e[k 7→ λx . reset0(X[x])]
Even though we can define this equivalence in our untyped
calculus, we cannot give a general typeto the shift0 operation in
our system. To generally type shift and reset operations a more
expressivetype system with answer types is required [1, 7]. Kammar
et al. [16] also show that it is possible togo the other direction
and implement handlers using delimited continuations.
4.4. PropertiesIt is shown in [28] using techniques from [47]
that well-typed programs cannot go wrong:
Theorem 1. (Semantic soundness)If · ⊢ e : τ | ϵ then either e ⇑
or e 7→⟩ v where · ⊢ v : τ | ϵ .where we use the notation e ⇑ for a
never-ending reduction. Another nice property that holds is:Lemma
1. (Effects are meaningful)If Γ ⊢ Hl[opl(v)] : τ | ϵ , then l ∈ ϵ
.This is a powerful lemma as it states that effect types cannot be
discarded (except through handlers).This lemma also implies effect
types are meaningful, e.g. if a function does not have an exc
effect, itwill never throw an exception.
5. OPTIMIZING TAIL RESUMPTIONSFrom the reduction rules, we can
already see some possible optimizations that can be used tocompiler
handlers efficiently. For example, if a handler never resumes, we
can treat it similarlyto how exceptions are handled and do not need
to capture the execution context (there are somecaveats here and we
come back to this is Section 8 on finalization).
An important other optimization applies to tail resumptions,
i.e. a resume that occurs in the tailposition of an operation
clause. In that case one implementation strategy could be to look
up theinnermost handler but not unwind the stack, and instead
directly execute the operation branchin place. As its last action
will be to resume, we now do not need to restore the stack but
insteaddirectly return the result in place.Since almost all
operations are tail-resumptive in practice, this is a huge
opportunity for an
efficient implementation, effectively turning most operations
into dynamically scoped method calls.The main cost is then looking
up the operations in the dynamic handler stack. Currently, one of
themost efficient implementations in C can do 150 million of such
operations per second on a Core i7@ 2.6GHz [27]. One can imagine
removing even the search cost by having a static ‘virtual
method
12
-
Extended Syntax:
Expressions e ::= . . .| skipl(e) skip frame
Extended Evaluation Contexts:
E ::= . . .| skipl(e)
Hl ::= . . .| handlel′h (Hl
′[skipl′(Hl)])
Extended Reduction Rules:
(handlet) handlelh · Hl · opl(v) −→ handlelh · Hl · skip
l · resume(e)[x 7→ v]with (op(x) → resume(e)) ∈ h
resume ∈/ fv(e)
(resumet) handlelh · Hl · skipl · resume(v) −→ handlelh · Hl ·
v
Fig. 6. Skip frames.
table’ per effect that is updated by its handlers at runtime
such that tail-resumptive operationstruly become comparable to
virtual method calls.
5.1. Skip FramesHowever, implementing tail-resumptive operations
in this way requires special skip frames toensure that any
operations called inside an operation clause are handled by the
correct handler. Inparticular, since we leave the stack in place,
we need to remember to ignore the part of the handlerstack up to,
and including, the handler that handles the operation. This is
formalized in Figure 6where we use the dot notation introduced in
Section 4.1.
We extend the syntax with skipl(e) frames that inactivates any
handler up to, and including,the innermost handlelh frame. This is
captured in the extra clause for the H
l contexts with thegrammar rule handlel
′h · Hl
′ · skipl′ · Hl . Since there is no other way to handle through
a skip frame,this ensures that no operations can be handled by any
frames in the skip range.There are two new reduction rules that
capture the optimization. The (handlet) rule can now
apply to tail-resumptive operations where the branch is of the
form op(x) → resume(e) whereresume ̸∈ fv(e). When this is the case,
the reduction rule leaves the stack as is, and only pushes askipl
frame and directly evaluates the branch in place. Once the branch
is evaluated, the (resumet)rule takes care of popping the skipl
frame again when it encounters the (unbound) resume call.
5.2. Soundness of Skip FramesWewould like to show that the
optimization is sound: i.e. if we evaluate with the optimized
(handlet)rule we get the same results as if we would have used the
plain (handle) rule.
13
-
To show this formally we use a subscript t to distinguish our
new optimized reduction rules −→tform the original reduction rules
−→, and similarly for the expression and evaluation contexts.
Wealso define an ignore function denoted by a small overline, on
expressions, e, and contexts Et andHlt . This function removes any
handle
lh · Hlt · skip
l sub expressions, effectively turning any of ourextended
expressions into an original one, and taking Et to E, and Hlt to
H
l . Using this function, wecan define soundness as:
Theorem 2. (Soundness of Skip Frames)If Et · et −→t Et · e′t
then Et · e −→ Et · e′.
Useful properties of the ignore function are v = v, and Et ·
handlelh · Hlt · skipl equals Et .
Proof. (Of Theorem 2) We show this by case analysis on reduction
rules. The first five rules areequivalent to the original rules.
For (handlet) we have:
Et · handlelh · Hlt · op(v)=
Et · handlelh · Hlt · op(v)=
Et · handlelh · Hlt · op(v)
−→
Et · e[x 7→ v, resume 7→ λy. handlelh · Hlt · op(y)]
= { resume ∈/ fv(e) }Et · e[x 7→ v]= { op ∈ h }
Et · handlelh · Hlt · skipl · e[x 7→ v]
=
Et · handlelh · Hlt · skipl · e[x 7→ v]
In the (resumet) rule, we know by construction that in the
original reduction rules, resume wouldhave been bound as a regular
resume 7→ λy.handlelh · Hlt · y:
Et · handlelh · Hlt · skipl · resume(v)
=
Et · handlelh · Hlt · skipl · resume(v)
=
Et · resume(v)=
Et · (λy.handlelh · Hlt · y)(v)−→
Et · handlelh · Hlt · v
□
14
-
Extended Syntax:
Expressions e ::= . . .| injectl(e) effect injection
Extended Type Rules:
S(l) = {op, . . .} Γ ⊢ op : τ1→ ⟨ι⟩ τ2 Γ ⊢ e : τ | ϵΓ ⊢
injectl(e) : τ | ⟨ι |ϵ⟩
[inject]
Extended Evaluation Contexts:
E ::= . . . | injectl(E)
Hl ::= . . .| injectl′(Hl) if l , l′
| handlelh(Hl[injectl(Hl)])
Extended Reduction Rules:
(inject) injectl(v) → v
Fig. 7. Extension with injection
6. INJECTIONAs observed by Biernacki et al. [3], to make
algebraic effect handlers truly composable we need aninject
function to inject effect types into the current effect row. For
example, function f takes anaction as an argument:fun f( action :
() → ⟨exn|e⟩ a) : e maybe⟨a⟩ { // inferrred
try {
setup()
Just(action())
}
fun(exn) {
Nothing
}
}
Suppose f only wants to handle exceptions raised by its own code
(e.g. setup) but not any exceptionsraised by action. As it is
defined, f now handles any exception raised by action too. The
solutionis to use inject the exception effect into the effect of
action:fun f( action : () → e a ) : e maybe⟨a⟩ { // inferred
try {
setup()
15
-
Just( inject⟨exn⟩{action()} )}
fun(exn) {
Nothing
}
}
Any exceptions raised by action will now propagate outside of f
– i.e. inject has a runtime effecttoo as it needs to skip the
exception handler in f when an exception is raised in action.
Figure 7 shows the extended syntax, evaluation contexts, and
runtime reduction rules for inject.An inject expression injectl(e)
injects the effect l into the effect of the expression e as shown
in thetype rules. Here we introduced an extra relation Γ ⊢ l : ι
that derives a full type from just aneffect constant l as effects
can generally have other parameters.
The reduction rule for injection does nothing: injectl(v) −→ v;
Indeed, the essence of inject is inthe extended handler contextH.
There are two new rules. The first one injectl′(Hl)with l , l′
statesthat inject frames can be ignored if the effect labels
differ. The second rule, handlel · Hl · injectl · Hl ,now allows a
handler for l to pass over another handler for l as long as that
handler is matched toan injectl .The use of handler Hl contexts
allows us to concisely capture this notion without having to
introduce explicit nesting levels. Biernacki et al. [3]
formalize handler contexts using nesting levelswhere a handler can
handle an operation if the context is “0-free”. These approaches
are equivalentand we can show that any handler context is 0-free by
construction:
Theorem 3. (Handler contexts are 0-free)For any Hl , we have
0-free(l,Hl).We use the derivation rules of n-freeness in shown by
Biernacki et al. [3] in Figure 2. To prove ourtheorem, we need the
following lemma:
Lemma 2. (Free extension)If n-free(l, E1) and m-free(l, E2),
then (n +m)-free(l, E1[E2]).This can be proved by straightforward
induction over the length of the n-free(l, E1) derivation andthe
structure of the evaluation contexts.
Proof. (Of Theorem 3) The proof proceeds by induction over the
structure of Hl . Most cases areimmediate – the interesting cases
are the handle and inject rules:• handlel · Hl1 · injectl · Hl2: by
hypothesis, Hl1 and Hl2 are 0-free (1). injectl · Hl2 is 1-free,
andtherefore by (1) and Lemma 2, Hl1 · injectl · Hl2 is also
1-free. It follows by the handle rulethat the full expression is
0-free.• injectl′ · Hl , and handlel′h · Hl with l , l′ (1): by
hypothesis, Hl is 0-free, and with (1) itfollows by the injection
rule that the full expression is 0-free. □
As a consequence, the results of Biernacki et al. [3], and in
particular the step-indexed relationalinterpretation of the
calculus, carry over to our formalization without further
changes.
6.1. Injection and ReturnBiernacki et al. [3] remark that with
injection we do not a special rule for return and can
translate:
handlel{h; return x → er }(e)
16
-
as a shorthand for:
handlel{h}(val x = e; injectl(er ))
(although this would require the (value) rule in this case).
However, this technique does not workfor parameterized handlers
where the return expression er can reference the handler
parameter.For this reason, we prefer our original interpretation in
Section 4.2 of return clauses as regularoperations.
6.2. Generative Effect TypesUsing inject also allows us to
program with first-class effect handlers. As a running example,
weare going to model a heap with polymorphic references:effect
heap⟨s,a⟩ {
fun get() : a
fun set( x : a ) : ()
}
The s parameter is a phantom type [30] that is used to emulate
generative types [40]. The handlerthat creates fresh references
uses a parameterized handler for the local state, but also provides
anexplicit type signature for the action:fun new-ref( init : a,
action : forall⟨s⟩ () → ⟨heap⟨s,a⟩|e⟩ b ) : e b {
handle(action) (st = init) {
get() → resume(st,st)set(x) → resume((),x)
}
}
The action gets a polymorphic rank-2 type which ensures that we
can only pass actions that arefully polymorphic in the s type, i.e.
that are parametric with respect to the heap. Note that the useof a
higher-rank type here is different for the use in the Haskell ST
monad [22]: there the rank-2parameter is used to ensure a reference
cannot escape the heap scope but that is already done herethrough
the effect system itself. In our case, we use it purely to work
with multiple references atthe same time.We can now use inject to
use a particular heap handler. For example, the get2 function
selects
the values of the innermost two heap handlers:fun get2() :
⟨heap⟨s1,a⟩, heap⟨s2,b⟩ | e⟩ (a,b) {
( get(), inject⟨heap⟩{get()} )}
Note that due to the Uneq-Label rule in Figure 3 each heap
effect is ordered in the final effect row.The inject construct now
allows us to select a particular handler that is reflected in the
effect type.For example:fun test() : ⟨ ⟩ int {
new-ref( 42, {
new-ref( False, {
val (x,y) = get2()
(if (x) then 1 else 0) + y
})
})
}
17
-
6.2.1. Use and Using keywords. Writing such nested higher-order
functions can be a bit cum-bersome and Koka provides the using and
use keywords to make this more convenient. Theseconstructs are just
syntactic sugar, where:using f (e1,. . .,en); body { f (e1,. .
.,en, fun(){ body })use x = f (e1,. . .,en); body { f (e1,. . .,en,
fun( x ){ body })With this new syntactic sugar we can write our
example more concisely as:fun test() : ⟨ ⟩ int {
using new-ref( 42 )
using new-ref( False )
val (x,y) = get2()
(if (x) then 1 else 0) + y
}
In this example, the type of x is inferred to be bool because of
the way the handlers in new-ref areordered. If we would swap their
order, the example would no longer type check! Since the
typeinference cannot swap the order of the heap effect types
automatically (due to Uneq-Label), weneed to do this manually:
fun bypass( action : () → ⟨heap⟨s2,b⟩, heap⟨s1,a⟩, heap⟨s2,b⟩ |
e⟩ c) : ⟨heap⟨s1,a⟩, heap⟨s2,b⟩ | e⟩ c {handle(action) {
get() → resume( inject⟨heap⟩{get()} )set(x) → resume(
inject⟨heap⟩{set(x)} )
}
}
fun test-swapped() : ⟨ ⟩ int {using new-ref( False )
using new-ref( 42 )
using bypass
val (x,y) = get2()
(if (x) then 1 else 0) + y
}
Here the bypass function explicitly handles first heap⟨s2,b⟩
effect by forwarding to the handlerfurther down using inject to
skip over the heap⟨s1,a⟩ handler. The bypass handler is inserted
afterthe swapped new-ref handlers to make the rest of the program
well-typed.Using inject, we are now able to explicitly refer to a
particular handler and can even express
first-class polymorphic references. However, there are clearly
various drawbacks because eachreference is expressed as a fresh
heap effect in the effect row.(1) Since every reference is
expressed in the effect type, this may lead to very large types
that
are difficult to understand. (This also precludes a dynamic
number of references; howeverthis is not a real limitation as a
dynamic number of references are of a particular type and wecan
model that using a handler that uses a list of references of that
type as its local state.)
(2) To use a particular reference, we need to insert just the
right amount of inject expressionsto select it.
(3) To use abstractions we need to manually commute heap effects
through the effect row.In the following section, we are going to
address these issues by using explicit values to refer toparticular
handlers instead of expressing it in the type only.
18
-
7. RESOURCESIn this section we are addressing the shortcomings
of first-class handlers using inject expressionsalone. The main
idea is to enable referencing a particular handler through a
regular value, called aresource. Building on our previous heap
example, we like to give the following type to the
heapoperations:get : (r : ref⟨a⟩ ) → ⟨heap,exn⟩ aset : (r : ref⟨a⟩,
x : a ) → ⟨heap,exn⟩ ()
All operations are now under a single ‘umbrella’ effect
heapwhile the particular handler is identifiedthrough a resource of
type ref⟨a⟩. The main drawback of this approach is that we lose
some safetyguarantees: because the particular handler is no longer
reflected in the effect type, it is not staticallyguaranteed that
the resource handler is in scope and an operation might fail
dynamically at runtimewith an exception. This is the reason for the
additional exn effect. We feel though this is a reasonabletradeoff,
similar to allowing partial functions like head. Note that even
though we use the samename, our semantics of a resource is quite
different from the resources that were originally presentin the Eff
language [2] – we discuss this further in Section 7.5.
In Koka, we can define resource effects as:effect heap { /*
empty */ }
effect resource ref⟨a⟩ in heap {fun get() : a
fun set(x : a) : ()
}
This declares an empty effect heap for the umbrella effect; in
general this effect can also declare itsown operations. For
example, for file resources, the umbrella filesystem effect might
provide theprimitive operations necessary for handling files. The
effect resource declaration declares a newresource ref⟨a⟩ under the
heap effect with the given operations. The handler for the heap
effect istrivial:val heap = handler⟨heap⟩{ }
We can create a fresh resource by creating a handler for it:fun
new-ref( init : a, action : (ref⟨a⟩ → ⟨heap|e⟩ b) ) : ⟨heap|e⟩ b
{
handle resource (action) (st = init) {
get() → resume(st,st)set(x) → resume((),x)
}
}
We use again a parameterized handler to model the state of the
reference. Because Koka infers thatwe handle a resource effect, the
action parameter has an inferred type that takes a fresh
resourceref⟨a⟩ as its first argument.Using resources, we no longer
need to use intricate inject expressions but can simply use the
resources as an extra argument to the operations (where we use
the use keyword as described inSection 6.2.1):fun test() : exn int
{
using heap
use x = new-ref(42)
use y = new-ref(False)
19
-
(if (y.get()) then 1 else 0) + x.get()
}
This is a big improvement compared to the approach based on
inject. As remarked before though,the price is the appearance of
the exn effect that signifies that we can no longer statically
guaranteethat a handler for a specific resource is in scope. Here
is an example of a program that would fail atruntime with an
exception:fun wrong() : ⟨heap,exn⟩ ref⟨int⟩ {
use x = new-ref(42)
x // escapes the scope of ‘new-ref‘
}
fun ouch() : exn int {
using heap
val y = wrong()
y.get()
}
7.1. Overriding Resource HandlersThe new-ref function now
creates a handler for a fresh resource, but it is also possible to
override anexisting handler for a resource. For example, we could
track the contents of a particular reference:fun track( r :
ref⟨int⟩, action : () → ⟨heap,io|e⟩ a) : ⟨heap,io|e⟩ a {
println("initial content: " + r.get().show )
handle resource r (action) {
get() → resume( r.get() ) // pass-throughset(x) → {
println("new content: " + x.show )
resume( r.set() ) // pass-through
}
}
}
By passing an extra argument r to the handle resource
expression, we are handling that specificresource instead of
creating a fresh one. Any operation on r in the action will now be
handled bythis handler. In the definition of each branch, we can
ourselves use operations on r that are handledby the original
handler. This gives us a very powerful extension mechanism to
override behaviorlocally in a structured and typed way.
7.2. Injecting ResourcesNow that we can override handlers for
resources, it is natural to ask if it would be useful toalso
‘inject’ a resource effect and being able to skip over the
innermost resource handler. Thisfunctionality seems a bit far
fetched but turns out to be very useful in practice.Bračevac et al.
[4] describe a reactive framework based on algebraic effect
handlers. This work
originally represented each event stream as a separate effect
type but proved to be too cumbersomein practice. The extension of
Koka with first-class resources was originally done to overcome
theselimitations. Now, each event stream can be represented using a
resource instead. Essentially, theCorrL framework defines:// empty
umbrella effect
effect corrl { }
20
-
effect resource stream⟨a⟩ in corrl {fun push( x : a ) : ()
...
}
An important function in the CorrL framework is alignment. The
align2 takes two streams andaligns them: it waits until each stream
performs a push and then actually performs both pushesand continues
evaluation of each stream in an interleaved way. One way to
implement this is touse local state and bypass each stream handler
to wait until both have pushed:
fun align(s1 : stream⟨a⟩, s2 : stream⟨b⟩, action : () →
⟨corrl,pure|e⟩ () ) : ⟨corrl,pure|e⟩ (){
var st1 := Nothing
var st2 := Nothing
val h1 = handler resource s1 {
push(x) → match(st2) {Nothing → st1 := Just((x,resume)) // and
do not resumeJust((y,resumey)) → {
s2.push(y)
s1.push(x)
interleaved { resume(()) } { resumey(()) }
}
}
}
val h2 = handler resource s2 {
push(y) → match(st1) {Nothing → st2 :=
Just((y,resume))Just((x,resumex)) → {
s2.push(y)
// subtle: skip the outer ‘h1‘ handler!
s1.inject-resource{ s1.push(x) }
interleaved { resumex(()) } { resume(()) }
}
}
}
h1{ h2( inject-st(action) ) }
}
Here we use two local references to store whether either
resource stream has been pushed to. Onceboth have been pushed to,
we propagate the push to the original resource handler, and
continueboth streams in an interleaved way.But there is twist: in
the inner h2 handler, when we want to push the value for s1 we
need
to skip over the h1 handler as we want to push to the original
handler for s1! Therefore, we useinject-resource to ‘inject’ a
resource effect. This is automatically provided by Koka for
eachresource effect – here it has type:
21
-
Extended Syntax:
Expressions e ::= . . .| new handlech(v) resource handler
Runtime effects l ::= l effect constants| x variables
Extended Reduction Rules:
(new) new handlech(v) −→ handlelh(v(l)) with fresh effect
constant l of type c⟨τ1, . . .,τn⟩
Extended Type Rules:
Γ ⊢ x : c⟨τ⟩ | ⟨⟩ Γ ⊢ e : τ | ϵ S(c) = {op1, . . ., opn}Γ, xr :
τ ⊢ er : τr | ⟨ι |ϵ⟩ Γ ⊢ opi : (c⟨τ⟩, τi) → ⟨ι, exn⟩ τ ′i | ⟨⟩Γ,
resume : τ ′i → ϵ τr , xi : τi ⊢ ei : τr | ⟨ι |ϵ⟩
Γ ⊢ handlex{ op1(x1) → e1; . . .; opn(xn) → en; return xr → er
}(e) : τr | ⟨ι |ϵ⟩[Handle-Var]
Γ ⊢ x : c⟨τ⟩ | ⟨⟩ S(c) = {op, . . .}Γ ⊢ op : (c⟨τ⟩, τ1) → ⟨ι,
exn⟩ τ2 Γ ⊢ e : τ | ⟨ι |ϵ⟩
Γ ⊢ injectx(e) : τ | ⟨ι |ϵ⟩[Inject-Var]
Γ, x : c⟨τ⟩ ⊢ handlex(e(x)) : τ | ϵ fresh x, τΓ ⊢ new handlec(e)
: τ | ϵ
[Handle-New]
Fig. 8. Extension with first-class resources
inject-resource : stream⟨a⟩ → (() → ⟨corrl|e⟩ a) : ⟨corrl|e⟩
aSince resource effects are under an umbrella effect, the inject of
the resource is not reflected inthe effect type; however, it has a
runtime effect in that it will skip the inner-most handler forthe
resource just like injection for a regular effect. Writing the
align function without resourceinjection is possible but more
involved as one has to define an outer handler that joins both
pushesand performs a push to the original handlers outside of the
bypass handlers.
7.3. Formalizing ResourcesIt turns out that enabling first-class
resources requires just minimal extensions to our originalsemantics
as shown in Figure 8. We first add a new syntactic expression new
handlec(v) is theequivalent of handle resource in Koka and creates
a fresh resource of type c⟨τ⟩ and handles it. Forsimplicity we only
allow value expressions v as the argument (usually a lambda
expression). Thesecond extension is to allow variables x as runtime
labels l. This extension now allows us to handlespecific resources
using handlexh(e) (versus handle
lh(e)) and to inject resources using injectx(e).
The operational semantics get one new transition rule (new) for
new handlech(v). This createsa fresh effect label l to identify the
handler/resource uniquely at runtime and simply transitions
22
-
to a handlelh(v(l)) expression where the argument v gets passed
the new resource l as a runtimeargument (of type c⟨τ1, . .
.,τn⟩).
All the other reduction rules are unchanged! The rules just
depend on runtime labels l and keepworking even if the labels are
sometimes dynamically generated through the (new) rule. However,not
all proofs work as before. In particular, the proof of Lemma 1.b is
no longer valid: the reductioncan get stuck for dynamic labels if
they escape the scope of their handler. This is the reason
foradding the exn effect to the type of their operations. In the
operational semantics we may add afurther rule that reduces
unhandled resource operations to raising an exception.
Since the basic reduction rules are unchanged, this means we can
understand and reason aboutresources just like regular effect
handlers – there is no intrinsic extra complexity.
7.4. Type RulesThere are quite a few new type rules in Figure 8
for the resources since we need to add new rulesfor each syntactic
construct that can now take a variable instead of just a constant
effect label.The Handle-Var rule describes a resource handler and
is quite similar to the Handle rule for effectconstants. The main
difference is that the operations now take an extra first argument
that is theresource of type c⟨τ⟩ and the effect of the operations
include the exn effect. Moreover, in contrastto Hanlde, a resource
handler does not discharge any effect and just passes the umbrella
effectthrough.
The Handle-New rule just binds a new resource variable and
refers to the Handle-Var rule to dothe actual type check. Finally,
the Inject-Var rule derives the umbrella effect ι by looking at
thetypes of the operations of the resource effect. In contrast to
Inject no actual effect type is injectedthough.
7.5. Resources in EffThe Eff language originally [2] also had
resources inspired by a co-algebraic interpretation ofeffects,
although they are currently removed from the language (but may come
back). However, thesemantics of such resources was not specified in
terms of ordinary handlers: instead they werehandled by default
handlers in the outer scope of the program and operation clauses
could not issueoperations themselves.
Recent work by Kiselyov and Sivaramakrishnan [19] embedded Eff
directly in the OCaml languageand they describe a more generalized
notion of effect resources that is more similar to our
semantics.They use a universal New effect as an umbrella effect for
all resources to create fresh instances. Theydo not describe
overriding resource handlers or resource injection but we believe
it is possible toextend their work to allow for this. Of course, in
that work there are no effect types as such, andthe embedding is
specified in terms of delimited continuations so it is hard to
compare directlyagainst our type system and semantics. In both
approaches though we can understand resources interms of regular
effect handlers which is a nice property.
7.6. Linear ResourcesAs another example, we look at modeling
external resources with linearity constraints, like files.The
umbrella effect is the filesystem with various low-level functions
to handle files:abstract effect filesystem {
fun fopen( path : string ) : fhandle
fun fread( h : fhandle ) : string
fun fclose( h : fhandle ) : ()
}
23
-
val filesystem : (action:() → ⟨filesystem|e⟩ a) → ⟨io|e⟩
=handler {
fopen(path) → resume( std/os/fopen(path) )fread(h) → resume(
std/os/fread(h) )fclose(h) → resume( std/os/fclose(h) )
}
The umbrella effect is declared abstract to make the internal
operations private to the module. Anice property of the filesystem
handler is that it makes the interface explicit: the action
parameterhas the filesystem effect while the handler gets the broad
io effect. This makes reasoning aboutpotential side effects much
more precise.
The resources in the filesystem are files with just a single
read operation:effect resource file in filesystem {
fun read() : string
}
fun file( path : string, action : file → ⟨filesystem|e⟩ a) :
⟨filesystem|e⟩ a {val h = fopen(path)
val x = handle(action) {
read() → resume( fread(h) )}
fclose(h)
x
}
The file handler opens a specific file, and provides a read
operations that reads from the openedfile handle. After the action
is done, the file handle is closed the final result x is returned.
Forexample:fun file-length( path : string ) : io int {
using filesystem
use f = file(path)
f.read().count
}
Clearly, the file handler is not ideal is it does not close open
file handles if an exception occursinside the action. It would not
suffice though to install an exception handler – any operation
calledin action might be handled by a handler that simply does not
call resume! Moreover, what shouldhappen if an operation is resumed
more than once and the as a result, the file handle might beclosed
more than once?
Dealing with linear resources is a gnarly problem for general
effect handlers and we tackle it inthe next section.
8. FINALIZATIONAs we saw in the previous section, we need a
general mechanism to finalize (and initialize) linearresources that
is not specific to exceptions since any handler might decide not to
resume, or resumemore than once. In particular, in Koka we can
rewrite the file handler of the previous section as:fun file( path
: string, action : file → ⟨filesystem|e⟩ a) : ⟨filesystem|e⟩ a
{
handle(action) (h) {
24
-
initially → fopen(path)finally → fclose(h)read() → resume(
fread(h) )
}
}
Here we have two new special branch constructs: initially and
finally that can be part of anyhandler. The initially branch is run
before the action is called and initializes the parameterizedstate
(h in the example). The finally branch is always executed when ‘the
scope is exited’ and isused to close any external resources.
To bemore precise, finally branches should be executed when
either the action returns normally,or if any operation in action
does not resume. The second situation is a bit tricky as it cannot
bedetected statically if a handler will resume or not. For example,
when modeling asynchrony orconcurrency [9–11, 26] the handler
usually stores the resume function in a scheduler queue andcalls
those later according to some particular schedule. Another example
is in Section 7.2 in thealign function that stores the resumptions
before invoking them later. At that point, a compilercannot
statically determine if such resumption will ever be called or
not.
We therefore propose that handlers that really do not resume,
call instead a new finalize functionthat ensures that all finally
branches under it are executed before returning. This approach
alsohas the advantage that the original semantics of algebraic
effect handlers stays the same. As anexample, the proper
implementation of a handler that turns exception effects into a
maybe type isnow:val to-maybe = handler {
return x → Just(x)raise(msg) → finalize(Nothing)
}
The argument to the finalize function is returned as its final
result after running all finallybranches and is there to mimic the
resume function (and just like resume, finalize is an
implicitlybound first-class function). Moreover, just like resume,
a tail-resumptive finalizer can be implementedmore efficiently.
Note that we could still choose to not use finalize and immediately
return withNothing – but in that case no finalizers are run (as per
the original semantics of algebraic effecthandlers).
8.1. Formalizing FinallyFigure 9 extends the original semantics
with finalizers. For now, we just extend with finally anddiscuss
initially clauses later on. The syntax is extended to allow for
finally → e clauses besidesthe return clauses. If unspecified,
finally → () is used by default.The new (handlef ) rule now binds
an extra finalize identifier that, instead of resuming, runs
all finalizers in the context. It does that by actually calling
resume but with a special cancell(v)expression (which can be
regarded as a special builtin exception). The evaluation rule
(cancel) forthe “cancel” expression simply propagates the cancel
value up through evaluation contexts F –these are just like regular
evaluation contexts but don’t include handle frames.
There is a formalization wrinkle here since finalize calls the
resume function r with a cancell(e)expression where we want to
substitute the cancell(e) expression as is without yet evaluating
it.
25
-
Extended Syntax:
Expressions e ::= . . .| cancell(e) cancel “exception”
Clauses h ::= finally→ e; return x → e finally and return
clauses| op(x) → e operation clause
Finalization Contexts:
F::= □ | F(e) | v(F) | val x = F; e | injectl(F)
Extended Reduction Rules:
(handlef ) handlelh · Hl · opl(v) −→ e[x 7→ v, resume 7→ r,
finalize 7→ f )]where(op(x) → e) ∈ hr = λλy. handlelh · Hl · yf =
λλy. r[cancell(y)]
(cancel) F · cancell(e) −→ cancell(e)
(unwind) handlel′h · cancell(e) −→ ef ; if l = l′ then e else
cancell(e)
where(finally→ ef ) ∈ h
(canceli) injectl′ · cancell(e) −→ if l = l′ then
cancell(cancell(e)) else cancell(e)
Fig. 9. Extension with (shallow) finalization. (This is refined
in the next section)
We therefore define the resume function with an internal lambda
function (λλ) and apply it usingsquare brackets as r[cancell(e)]
outside our regular reduction rules2.The (unwind) rule handles the
situation where a cancell(v) value meets a handler frame by
unwinding through all the handlers and executing their
finalizers in sequence until the originalhandler l that issued the
finalize is found.For "return x" clauses we need to extend our
previous syntactic sugar as defined in Section 4.2,
and redefine:
handlel{h; return x → er }(e)as syntactic sugar for an implicit
return operation that finalizes:
handlel{h; return(x) → finalize(er )}(return(e))This ensures
that after evaluating the return expression er , we will also run
our own “finally” clause.In the next section we discuss further
optimizations of this rule.2For convenience we also use a λλ as a
regular λ if they are applied only to values but it would be more
correct to bind theresume to λx . r[x] and finalize to λx . f [x]
instead of just r and f respectively.
26
-
Extended Syntax:
Expressions e ::= . . .| protectv(e) protect frame
Extended Contexts:
E::= . . . | protectf (E)Hl ::= . . . | protectf (Hl)
Extended Reduction Rules:
(handled) handlelh · Hl · opl(v) −→ protectf · e[x 7→ v, resume
7→ r, finalize 7→ f ]where(op(x) → e) ∈ hr = λλy. handlelh · Hl ·
yf = λλy. if |r | = 0 then r[cancell(y)] else y
(protect) protectf · cancell(e) −→ f [cancell(e)](unprotect)
protectf · v −→ v
(unwind) handlel′h · cancell(e) −→ ef ; if l = l′ then e else
cancell(e)
where(finally→ ef ) ∈ h
Fig. 10. Update of Figure 9 with deep finalization. The (unwind
) rule is repeated for completeness.The expression |r | returns the
number of calls to function r .
There are some subtle corner cases when running finalizers. In
particular, the expression ef in(unwind) should not be able call
any operations in the handler that called finalize: that would
causean infinite loop as the same handler would be pushed over and
over again! This is prevented in the(unwind) rule by not running
the finally expression under the handler that defines it.
Another situation that might occur is that another operation is
called in a finally clause duringunwinding, that itself calls
finalize. This situation is a bit like throwing an exception inside
anexception handler itself. In that case, a new unwinding will take
place but since it finalizes itself, allfinally handlers will still
be run in sequence (it might cut a finalization short though if the
newhandler is under the original one).
8.2. Deep FinalizationUnfortunately, the finalization as
formalized in our previous section is not yet quite good enough.In
essence, we want to ensure finally clauses get run reliably
whenever an operation does notresume and calls finalize instead.
This may still fail though if a finalizing operation is called
fromwithin an operation clause itself! Illustrating this is a bit
complex, as we need at least 3 nestedhandlers to show this. For
example, assume we write a handler for asynchrony where we
storeresumptions in a queue [10, 29], somewhat like:
27
-
fun async( action ) {
handle(action) (queue = []) {
await(f) → {... // part A
// queue this resumption
val newqueue = queue.insert(resume,finalize)
... // part B
newqueue.schedule() // resume some other strand with an updated
queue
}
finally → {// finalize any resumption still in the queue
queue.foreach fun((resume,finalize)) {
finalize()
}
}
}
}
Now, suppose we use a file handler under the async handler,
running under the to-maybe handlerfrom the start of Section 8:using
to-maybe
using async
use f = file("path")
...
await(...)
If no exceptions happen, everything runs just fine and the
finally clause in the async handler ensuresthat any outstanding
strands are finalized if needed.Unfortunately, if the B part in the
async handler raises an exception, the handler for to-maybe
will call finalize but since we are in an operation clause, the
handle frame for file will not be onthe stack and the finally
clause is not executed! However, even if we would somehow execute
thefinally clause and finalize all outstanding strands, this still
goes wrong if an exception happens inpart A: in that case the await
resumption is not stored in the queue and the finally clause will
stillnot call finalize on it (which in turn means that the finally
clause of the file handler is neverinvoked).
8.3. Protecting Operation ClausesTo ensure that finally clauses
are reliably executed under finalization we need to protect
operationclauses. When finalization happens inside the clause of an
operation and the operation did notresume (or finalize) yet, then
we should implicitly invoke finalize for that operation clause. We
callthis deep finalization.
In the previous example, that means that even if an exception is
raised in part A, finalize is calledautomatically for that
operation which ensures all finally clauses are executed, including
the onefor the current handler (which also causes all outstanding
strands in the queue to be finalized).Moreover, we need to also
ensure that a finalize call only resumes and finalizes once, and
has noeffect if called again (or after a resume). For example, if
an exception occurs in part B, the operationclause automatically
calls finalize but the finalize was also already inserted into the
queue: whenthe finally clause runs this again causes a call to the
finalize function for that strand (which hasno effect this time
around).
28
-
This is all formalized in Figure 10. We introduce a new protectf
(e) stack frame that protectsthe expression e by running the
finalizer f if needed. When we look at the new reduction rule
forhandlers, (handled), the new finalization function f is defined
as:
f = λλy. if |r | = 0 then r[cancell(y)] else y
where the operation |r | returns the number of times the resume
function r has been called. Thisoperation is very easy to implement
in practice (using a mutable field that counts invocations)
butcumbersome to fully formalize as it requires that the function r
is shared (which we also signifywith the double lambda λλ). This
can be done by threading a separate environment through
thereduction rules that keeps track of shared beta reductions. This
is similar to formalizations for thelazy evaluation [21] or optimal
lambda reduction [20, 42]. For simplicity though we will treat the
|r |function here as an oracle without formalizing it fully. The
finalization function f now only resumeswith the cancell(y)
expression if the resume function was never invoked before, and
otherwise itbehaves like the identity function immediately
returning its result (e.g. much like a thunk in lazylanguages).The
evaluation of an operation clause is now under a protectf frame.
The rules (protect) and(unprotect) define its behavior where the
rule (unprotect) removes the protect frame when theoperation clause
returns with a result v (and thus protect frames behave like an
identity function).However, if the evaluation of the operation
clause leads to finalization, the rule (protect) ensures thatthe
finalize function f for that operation is invoked. If the operation
already resumed or finalizedbefore, the run once check in the
definition of the finalize function ensures that this happens
atmost once.
Note that the finalization function f is invoked with a
cancell(e) expression as the argument: if fresumes with a
cancellation itself (through (protect)), the two will be nested as
cancell(cancell′(e)),i.e. first finalize up to l and then continue
finalizing up to l′. This is also important if handlers forthe same
effect are nested within each other and we need to finalize each
one in order.Since protectf frames only have an effect if the
operation clause never resumes or finalizes, we
can optimize an implementation by squeezing out protect frames
once a resume or finalize happens.In particular, for any operation
where resume and finalize refer to same r as protectf , we
have:
protectf · F · resume(e)= F · resume(e)protectf · F ·
finalize(e)= F · finalize(e)
This also means that if an operation is tail resumptive and has
the form resume(e) or finalize(e),there is no need to push a
protectf frame in the first place.
8.4. Return as an OperationAs shown in Section 4.2 and 8.1, we
define return clauses as syntactic sugar for a return
operation:
handlel{h; return(x) → finalize(er )}(return(e))
For an implementation this might seem to induce too much
overhead by calling finalize on everyreturn. In practice though we
can optimize the implementation quite a bit. For example, the
finalizeon the return operation does not really need to resume but
just run the finally clause (if it exists)since Hl = □. Generally,
we can use the following direct rule for returning with deep
finalization:
29
-
(returnd) handlelh(v) −→ val y = protectg(e[x 7→ v]); ef ;
ywhere(return x → e) ∈ h(finally→ ef ) ∈ hg = λλy. (ef ; y)
Proof. We can prove this rule correct as follows; Starting from
the translated return clause, wehave:
handlelh · returnl(v)−→
protectf · finalize(er )[x 7→ v, resume 7→ r, finalize 7→ f
]−→
protectf · f (er [x 7→ v, resume 7→ r, finalize 7→ f ])= {
resume,finalize ̸∈ fv(er ) }
protectf · f (er [x 7→ v])
There are now three cases to consider: evaluation to a value, to
a cancellation, or divergence. Incase 1 the expression evaluates to
a value w, er [x 7→ v] −→∗ w:
protectf · f (er [x 7→ v])−→∗ { hypothesis }
protectf · f (w)−→ { |r | = 0, Hl = □}
protectf · handlelh · cancell(w)
−→protectf (ef ; w)= { now |r | > 0 }
ef ; w←−
val y = w; ef ; y←−
val y = protectg(w); ef ; y∗←− { hypothesis }
val y = protectg(er [x 7→ v]); ef ; y
In case 2 the expression evaluates to cancellation: er [x 7→ v]
−→∗ cancell′(ec):
protectf · f (er [x 7→ v])−→∗ { hypothesis }
protectf · f (cancell′(ec))
−→protectf · cancell
′(ec)−→
f [cancell′(ec)]= { |r | = 0 and Hl = □}
handlelh(cancell(cancell′(ec)))
−→
30
-
ef ; cancell′(ec)
←− (cancel)val y = (ef ; cancell
′(ec)); ef ; y=
val y = (λλy. ef ; y)[cancell′(ec)]; ef ; y
←−val y = protectg(cancell
′(ec)); ef ; y∗←− { hypothesis }
val y = protectf (er [x 7→ v]); ef ; yCase 3 for divergence is
similar. □
8.5. Deep Finalization and Skip FramesDeep finalization and skip
frames also work well together. For any tail resumptive operation,
weknow the last action taken is the resume (or finalize) which
entails that |r | is always 0. From thiswe can derive that we can
leave the (handlet) rule (Figure 6) unchanged and only need to add
anextra reduction rule for when a cancel frame meets a skip
frame:(resumec) skipl
′ · cancell(e) −→ cancell′(cancell(e))Since we know we never
resumed, a normal protect frame would cause finalization. With a
skipframe we are already under the right execution context and we
can simply propagate the existingcancellation by wrapping it which
is very efficient.
8.6. Formalizing InitiallyInitialization is the dual of
finalization: whereas finalizers are concerned with operations
thatdo not resume, the initializer is concerned with operations
that resume more than once. Initiallyclauses can be present only on
parametrized handlers where the give the initial value for
thehandler parameter, like the file handle in the our earlier
example. The idea is now to arrange thata initially clause is
re-executed whenever an operation under the handler is resumed more
thanonce. Operations that resume more than once are rare but do
occur in examples like probabilisticprogramming or backtracking
[18].
Figure 11 formalizes the initially clauses. Since initially
clauses can only be present on parame-terized handlers, we also
extend the syntax here with parameterized handlers together with
twonew evaluation rules for parameterized handlers that first
evaluate the initial value of the handlerparameter before
evaluation the action.
There is new reduction context R for rewinding through the stack
– it is equivalent to a regularevaluation context except it has no
cases for parameterized handlers. We have a special
identifierrewind(e) that rewinds an evaluation context and
re-executes any initially clauses in sequence. Therule (rewind)
uses the R context to find the outermost parameterized handler and
re-initializes thehandler parameter with the initially expression.
The (unrewind) rule terminates the unwindingcontinuing on from
there.The new reduction rule for handlers, (handlei), now applies
the rewind expression whenever it
resumes more than once. As described in the previous section, we
assume we have an operation |r |that returns the number of times
the resumption function r has been called.
8.7. Dynamic-windThe Scheme language always supported delimited
continuations and they have also struggled withinitialization- and
finalization for continuations that resumed more than once. The
unwind-protect
31
-
Extended Syntax:
Expressions e ::= . . .handlelh(p = e)(e) Parameterized
handler
Clauses h ::= initially→ e; finally→ e; return x → e| op(x) → e
operation clause
Extended Evaluation Contexts and Rewind Contexts:
E ::= . . .| handlelh(p = E)(e) | handle
lh(p = v)(E)
R::= □ | R(e) | v(R) | val x = R; e | injectl(R) |
handlelh(R)
Extended Reduction Rules:
(handlei) handlelh · Hl · opl(v) −→ protectf · e[x 7→ v, z 7→
vz, resume 7→ rw, finalize 7→ f ]with(op(x) → e) ∈ hr = λλy.
handlelh · Hl · yf = λλy. if |r | = 0 then r[cancell(y)] else yrw =
λλy. if |r | = 0 then r[y] else rewind · r[y]
(rewind) rewind · R · handlelh(p = vp) · e −→ R · handlelh(p =
ei)(rewind · e)
with(initially→ ei) ∈ h
(unrewind) rewind · R · v −→ R · v
Fig. 11. Extension with initialization
in Scheme is like a finally clause, while dynamic-wind is like
initially/finally with a pre- andpostlude [6, 12, 17, 35]. Sitaram
[43] describes how the standard dynamic-wind is not good enoughin
general: “While this may seem like a natural extension of the
first-order unwind-protect to ahigher-order control scenario, it
does not tackle the pragmatic need that unwind-protect
addresses,namely, the need to ensure that a kind of ‘clean-up’
happens only for those jumps that significantlyexit the block, and
not for those that are aminor excursion. The crux is identifying
which of thesetwo categories a jump falls into.”. Interestingly,
this is exactly what is addressed by algebraic effectswhere
“significant exit”s are operations that do not resume, while “minor
excursions” are regularoperations that resume with a result.
8.8. Safety of FinalizationThe semantics for deep finalization
and initialization address many requirements for robust handlingof
external resources (even in the presence of multiple resumptions).
Moreover, it is defined as adynamic untyped semantics and does not
depend on static properties like linearity constraints.Nevertheless
there are some aspects that we would like to improve upon:
32
-
(1) As defined, there is no guarantee that all finally clauses
are executed – this only happens ifoperation clauses that do not
resume use finalize correctly. This is not a bad property perséas
it is exactly what gives the expressive power to define
abstractions like asynchronousexecution. Nevertheless, in practice
one would like to prevent accidentally forgetting about afinalize
call. Actual implementations might opt for special syntax or static
checks to invertthis situation. For example by wrapping every
operation clause by default with an implicitfinalize to guarantee
proper finalization if resume (or finalize) was not called
explicitly; i.e.the programmer must declare explicitly the
intention to not doing finalization in an operationclause instead
of the other way around.
(2) The definition of resume and finalize behave differently on
the first invocation than later on.This is unsatisfying from a
semantic perspective (although still deterministic). As shown
inprevious sections, the property is important in practice but we
would like to see if is possibleto capture this behavior in a more
declarative way.
We would like to show that wrapping every operation clause with
a finalize leads to robustevaluation of every finally clause. First
define results w as:
w ::= v | cancell(e)Then we can state the following theorem:
Theorem 4. (Deep finalization is robust)For any evaluation that
does not get stuck or diverges (i.e. it evaluates to a w), and
where every oper-ation clause is of the form finalize(e), then, if
E · handlelh · e 7−→∗ w we have E · handle
lh · e 7−→∗
E′ · ef 7−→∗ w, where (finally → ef ) ∈ h.A formal proof of this
property is work in progress, but we have done an initial proof
sketch usinginduction over the number of handlers in an
expression.
9. CONCLUSIONWe presented a formal semantics for various
optimizations and extensions to algebraic effects thatare all
important for any practical implementations. We hope to experiment
more with first-classalgebraic resources and plan to investigate
more optimized implementations based on virtualmethod tables for
each effect.Finally, an attractive property of algebraic effect
handlers is that they have a solid semantic
foundation in category theory – but the extended rules for deep
finalization and initialization inthis article are “just”
operational reductions that we defined in a particular way. We hope
that itis possible to find again the foundational categorical
structures to capture the semantics of thesereduction rules and
that can lead to a more foundational framework for reasoning about
linearresources.
AcknowledgementsWe would like to thank Oliver Bračevac for
explaining the intricacies of reactive programming
with effect handlers leading to the design of first-class
resources.
REFERENCES[1] Kenichi Asai, and Yukiyoshi Kameyama. “Polymorphic
Delimited Continuations.” In Proceedings of the 5th Asian
Conference on Programming Languages and Systems, 239–254.
APLAS’07. Singapore. 2007. doi:10.1007/978-3-540-76637-7_16.
[2] Andrej Bauer, and Matija Pretnar. “Programming with
Algebraic Effects and Handlers.” J. Log. Algebr. Meth. Program.84
(1): 108–123. 2015. doi:10.1016/j.jlamp.2014.02.001.
[3] Dariusz Biernacki, Maciej Piróg, Piotr Polesiuk, and Filip
Sieczkowski. “Handle with Care: Relational Interpretation
ofAlgebraic Effects and Handlers.” Proc. ACM Program. Lang. 2
(POPL’17 issue): 8:1–8:30. Dec. 2017. doi:10.1145/3158096.
33
https://dx.doi.org/10.1007/978-3-540-76637-7_16https://dx.doi.org/10.1007/978-3-540-76637-7_16https://dx.doi.org/10.1016/j.jlamp.2014.02.001https://dx.doi.org/10.1145/3158096
-
[4] Oliver Bračevac, Nada Amin, Guido Salvaneschi, Sebastian
Erdweg, Patrick Eugster, andMiraMezini. “Effectful
ReactiveProgramming.” 2018.
http://www.st.informatik.tu-darmstadt.de/artifacts/corrl/corrl_draft.pdf.
Draftarticle.
[5] Jonathan Immanuel Brachthäuser, and Philipp Schuster.
“Effekt: Extensible Algebraic Effects in Scala.” In
Scala’17.Vancouver, CA. Oct. 2017.
[6] William D. Clinger. “Implementation of unwind-Protect in
Portable Scheme.” 2003.
http://www.ccs.neu.edu/home/will/UWESC/uwesc.sch.
[7] Olivier Danvy, and Andrzej Filinski. A Functional
Abstraction of Typed Contexts. DIKU, University of
Copenhagen.1989.
[8] Olivier Danvy, and Andrzej Filinski. “Abstracting Control.”
In Proceedings of the 1990 ACM Conference on LISP andFunctional
Programming, 151–160. LFP ’90. Nice, France. 1990.
doi:10.1145/91556.91622.
[9] Stephen Dolan, Spiros Eliopoulos, Daniel Hillerström, Anil
Madhavapeddy, KC Sivaramakrishnan, and Leo White.“Effectively
Tackling the Awkward Squad.” In ML Workshop. 2017.
[10] Stephen Dolan, Spiros Eliopoulos, Daniel Hillerström, Anil
Madhavapeddy, KC Sivaramakrishnan, and Leo White.“Concurrent System
Programming with Effect Handlers.” In Proceedings of the Symposium
on Trends in FunctionalProgramming. TFP’17. May 2017.
[11] Stephen Dolan, Leo White, KC Sivaramakrishnan, Jeremy
Yallop, and Anil Madhavapeddy. “Effective Concurrencythrough
Algebraic Effects.” In OCaml Workshop. Sep. 2015.
[12] Iulian Dragos, Antonio Cunei, and Jan Vitek. “Continuations
in the Java Virtual Machine.” Second ECOOP Workshopon
Implementation, Compilation, Optimization of Object-Oriented
Languages, Programs and Systems (ICOOOLPS’2007).Technische
Universität Berlin, Berlin. 2007.
[13] Ben R. Gaster, and Mark P. Jones. A Polymorphic Type System
for Extensible Records and Variants. NOTTCS-TR-96-3.University of
Nottingham. 1996.
[14] Daniel Hillerström, and Sam Lindley. “Liberating Effects
with Rows and Handlers.” In Proceedings of the 1st
InternationalWorkshop on Type-Driven Development, 15–27. TyDe 2016.
Nara, Japan. 2016. doi:10.1145/2976022.2976033.
[15] J.R. Hindley. “The Principal Type Scheme of an Object in
Combinatory Logic.” Trans. of the American MathematicalSociety 146
(December): 29–60. Dec. 1969. doi:10.2307/1995158.
[16] Ohad Kammar, Sam Lindley, and Nicolas Oury. “Handlers in
Action.” In Proceedings of the 18th ACM SIG-PLAN International
Conference on Functional Programming, 145–158. ICFP ’13. ACM, New
York, NY, USA. 2013.doi:10.1145/2500365.2500590.
[17] Richard Kelsey, William Clinger, and Jonathan Rees.
“Revised5 Report on the Algorithmic Language Scheme.” 1998.chapter
6.4.
[18] Oleg Kiselyov, and Chung-chieh Shan. “Embedded
Probabilistic Programming.” In Domain-Specific Languages.
2009.doi:10.1007/978-3-642-03034-5_17.
[19] Oleg Kiselyov, and KC Sivaramakrishnan. “Eff Directly in
OCaml.” In ML Workshop 2016. Dec. 2017.
http://kcsrk.info/papers/caml-eff17.pdf. Extended version.
[20] John Lamping. “An Algorithm for Optimal Lambda Calculus
Reduction.” In Proceedings of the 17th ACM SIGPLAN-SIGACT Symposium
on Principles of Programming Languages, 16–30. POPL ’90. San
Francisco, California, USA. 1990.doi:10.1145/96709.96711.
[21] John Launchbury. “A Natural Semantics for Lazy Evaluation.”
In Proceedings of the 20th ACM SIGPLAN-SIGACTSymposium on
Principles of Programming Languages, 144–154. POPL ’93. Charleston,
South Carolina, USA. 1993.doi:10.1145/158511.158618.
[22] John Launchbury, and Amr Sabry. “Monadic State:
Axiomatization and Type Safety.” In In Proceedings of the 2nd
ACMSIGPLAN International Conference on Functional Programming,
227–238. ICFP’97. 1997. doi:10.1145/258948.258970.
[23] Daan Leijen. “Extensible Records with Scoped Labels.” In
Proceedings of the 2005 Symposium on Trends in
FunctionalProgramming, 297–312. 2005.
[24] Daan Leijen. “Koka: Programming with Row Polymorphic Effect
Types.” In MSFP’14, 5th Workshop on MathematicallyStructured
Functional Programming. 2014. doi:10.4204/EPTCS.153.8.
[25] Daan Leijen. “The Koka Repository.” 2016.
https://github.com/koka-lang/koka, the extensions described in
thispaper are available in the dev branch with examples in the
test/resource directory.
[26] Daan Leijen. “Structured Asynchrony with Algebraic
Effects.” In Proceedings of the 2nd ACM SIGPLAN
InternationalWorkshop on Type-Driven Development, 16–29. TyDe 2017.
Oxford, UK. 2017. doi:10.1145/3122975.3122977.
[27] Daan Leijen. “Implementing Algebraic Effects in C.” In
Programming Languages and Systems, edited by Bor-Yuh EvanChang,
339–363. Springer International Publishing. 2017.
[28] Daan Leijen. “Type Directed Compilation of Row-Typed
Algebraic Effects.” In Proceedings