0% found this document useful (0 votes)
2 views12 pages

2013 Oleg ExtEff

The document presents a library for combining effects in functional programming without the limitations of monad transformers, allowing for dynamic interaction of effects. It introduces an extensible union type to track active effects and supports flexible combinations, enhancing expressiveness and efficiency. The library is designed for Haskell and aims to improve upon existing frameworks by allowing effects to be interleaved and managed more effectively.

Uploaded by

naqe0gngi5
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views12 pages

2013 Oleg ExtEff

The document presents a library for combining effects in functional programming without the limitations of monad transformers, allowing for dynamic interaction of effects. It introduces an extensible union type to track active effects and supports flexible combinations, enhancing expressiveness and efficiency. The library is designed for Haskell and aims to improve upon existing frameworks by allowing effects to be interleaved and managed more effectively.

Uploaded by

naqe0gngi5
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 12

Extensible Effects

An Alternative to Monad Transformers

Oleg Kiselyov Amr Sabry Cameron Swords


[email protected] Indiana University, USA Indiana University, USA
[email protected] [email protected]

Abstract Monad transformers is a framework to let the programmer as-


We design and implement a library that solves the long-standing semble monads through a number of transformers, producing a
problem of combining effects without imposing restrictions on their combined effect consisting of “layers” of elementary monadic ef-
interactions (such as static ordering). Effects arise from interactions fects. As effectful operations may combine in different ways, at
between a client and an effect handler (interpreter); interactions each layering the programmer manually expresses how the oper-
may vary throughout the program and dynamically adapt to exe- ations of the underlying, or base, monad ‘lift’ through the current
cution conditions. Existing code that relies on monad transform- transformer. (This idea is also investigated from a different perspec-
ers may be used with our library with minor changes, gaining effi- tive and in more depth by Filinski [7, 8].) In the standard implemen-
ciency over long monad stacks. In addition, our library has greater tation of monad transformers, each layer has overhead that adds up
expressiveness, allowing for practical idioms that are inefficient, over long stacks (see §4 for discussion). In addition, the layering
cumbersome, or outright impossible with monad transformers. is determined statically and cannot easily be altered dynamically in
Our alternative to a monad transformer stack is a single monad, different parts of the program. Most importantly, some practical sit-
for the coroutine-like communication of a client with its handler. uations require the effects to be interleaved, wherein no complete
Its type reflects possible requests, i.e., possible effects of a com- static layering of one effect over the other provides the desired se-
putation. To support arbitrary effects and their combinations, re- mantics.
quests are values of an extensible union type, which allows adding Thus, despite its popularity, the framework of monad transform-
and, notably, subtracting summands. Extending and, upon han- ers is fundamentally limited. (We review, with examples, these lim-
dling, shrinking of the union of possible requests is reflected in its itations in §5.) Alternative approaches to monad transformers ex-
type, yielding a type-and-effect system for Haskell. The library is ist [12, 19]; the most relevant to our work is the “extensible deno-
lightweight, generalizing the extensible exception handling to other tational language specifications” (EDLS) approach of Cartwright
effects and accurately tracking them in types. and Felleisen [3] that was developed at around the same time as
the original paper on monad transformers [18]. The basic idea is to
Categories and Subject Descriptors D.3.2 [Programming Lan- model an effect as an interaction: a program fragment that wishes
guages]: Language Classifications—Applicative (functional) lan- to update a mutable variable, throw an exception, or write to a file
guages; F.3.3 [Logics and Meanings of Programs]: Studies of Pro- sends a ‘request’ to an ‘authority.’ The request describes the action
gram Constructs—Control primitives; F.3.3 [Logics and Mean- to perform and contains the ‘return address,’ a continuation, to re-
ings of Programs]: Studies of Program Constructs—Type structure sume the requester. In the original EDLS framework, this authority,
i.e., the interpreter of requests as transformations on resources, is
Keywords monad, monad transformer, effect handler, open union, not part of the user program (just as the the operating system kernel
type and effect system, effect interaction, coroutine is not part of the user’s process, and the interpreter of IO actions
in Haskell is not part of the user program). This global external
1. Introduction authority is in charge of all the resources (files, memory, etc.): it
interprets a request, and may either execute it on the requester’s
From the early days of the introduction of monads to the world of behalf and then continue the requester, passing to it the result –
functional programming [22], it was understood that monads, gen- or decline to answer, thus aborting the requester. The fundamen-
erally, do not compose [14, 29, 34]. A variety of ‘monad compo- tal benefit of this approach is that the order of composing effects,
sitions’ were investigated based on Moggi’s idea of “monad mor- when it is irrelevant, does not need to be specified at all, and there is
phisms” [23]. Several initial designs [4–6, 29, 30] were extended by no need to commit to a single, statically determined order of effect
Liang’s et al. [18] in what has become the current state of the art for composition. The limitations are: (i) the global external authority is
Haskell: the “monad transformers library” (MTL) (mtl-2.1.2). difficult to extend, (ii) effects are not encapsulated, and (iii) effects
of a computation are not reflected in its type.
Permission to make digital or hard copies of all or part of this work for personal or Partially inspired by free monads [32] and the term algebra ap-
classroom use is granted without fee provided that copies are not made or distributed proach of Hughes and Hinze [9, 10], we address the three problems
for profit or commercial advantage and that copies bear this notice and the full citation above:
on the first page. Copyrights for components of this work owned by others than the
author(s) must be honored. Abstracting with credit is permitted. To copy otherwise, or • We replace the central, inflexible authority by a distributed,
republish, to post on servers or to redistribute to lists, requires prior specific permission automatically extensible, “bureaucracy” that is part of the user
and/or a fee. Request permissions from [email protected].
program. Following the work on algebraic handlers [1, 25], we
Haskell ’13, September 23–24, 2013, Boston, MA, USA.
Copyright is held by the owner/author(s). Publication rights licensed to ACM. call each partial authority (that controls some resources and
ACM 978-1-4503-2383-3/13/09. . . $15.00. interprets some requests) a handler. Each such handler is both
http://dx.doi.org/10.1145/2503778.2503791 the authority for its client part of the program and a client itself:
if a handler receives a request it does not understand, it relays instance Monad (Eff r)
the request to an “upstream” handler.
−− Pure computations
• Second, and more importantly, we develop an expressive type- data Void
and-effect system that keeps track of which effects are currently run :: Eff Void w → w
active in a computation. This system maintains an open union
(a type-indexed coproduct of functors) containing an unordered −− Reader (or environment) effect
type Reader e
collection of current effects. The action of each handler is ask :: (Typeable e, Member (Reader e) r) ⇒ Eff r e
reflected in the type by removing the effects that have been local :: (Typeable e, Member (Reader e) r) ⇒
handled, and thus the type system can guarantee that a full (e → e) → Eff r w → Eff r w
program does not contain “dangling effects.” runReader :: Typeable e ⇒
Eff (Reader e B r ) w → e → Eff r w
Our primary contributions are:
• a user-level effect framework modeled after the MTL syntax −− Exceptions
type Exc e
that allows effects to be combined in any order and even in- throwError :: (Typeable e, Member (Exc e) r) ⇒ e → Eff r a
terleaved in ways that are impossible in the standard monad catchError :: (Typeable e, Member (Exc e) r) ⇒
transformer approach; the system is realized as a Haskell li- Eff r w → (e → Eff r w) → Eff r w
brary using common extensions: developers may immediately runError :: Typeable e ⇒
start using it with minimal syntactic changes to existing pro- Eff (Exc e B r ) w → Eff r (Either e w)
grams;
−− State
• a detailed analysis of the expressiveness problems of monad type State s
transformers that cause subtle bugs in real programs; get :: (Typeable s, Member (State s) r) ⇒ Eff r s
put :: (Typeable s, Member (State s) r) ⇒ s →
• a novel implementation of an extensible, constant-time, open
Eff r ()
union type facility that sits at the center of the effect handler runState :: Typeable s ⇒
system; Eff (State s B r ) w → s → Eff r (w,s)
• a system built around the new open union facility that provides −− Non−determinism
mechanisms for adding, using, and removing effects dynami- type Choose
cally across programs, yielding an effect system far more ex- choose :: Member Choose r ⇒ [w] → Eff r w
pressive than the current Haskell approach based on the MTL. makeChoice :: Eff (Choose B r ) w → Eff r [ w]
To summarize, the resulting design improves on both monads trans- −− Tracing
formers and EDLS, facilitating more flexible interactions of ef- type Trace
fects that are accurately tracked by the type system. The com- trace :: Member Trace r ⇒ String → Eff r ()
plete Haskell implementation along with illustrations and warm- runTrace :: Eff (Trace B Void) w → IO w
up examples can be found at http://okmij.org/ftp/Haskell/
−− Built−in effects (e. g., IO)
extensible/. We shall refer to this code throughout the paper, type Lift m
quoting relevant excerpts. lift :: (Typeable1 m, MemberU2 Lift (Lift m) r) ⇒
The remainder of the paper is structured as follows. We begin m w → Eff r w
with a high-level “tour” of our extensible effects framework that runLift :: (Monad m, Typeable1 m) ⇒
introduces its programmer-level interface with a variety of small Eff (Lift m B Void) w → m w
examples (§2). The next section (§3) provides the full semantics
Figure 1. The interface of the library of extensible effects
and explains the key components of the implementation. Then, §4
confirms that our design can simulate the full MTL and §5 uses
several advanced examples to illustrate that our design goes beyond
this indicates that the effect m is an element of the set r. The second
the MTL in expressiveness. We conclude after a comparison with
is via an explicit pattern (m Br’) that decomposes the union r
related work.
into the effect m and the remaining effects r’; this is analogous
to {m} ∪ r0 . We may think of Void as ∅, i.e., a computation
2. A Tour of the Extensible Effects Framework of type (Eff Void a) is pure. In contrast, a computation of type
We start with a few examples to give a feel for our library of (Eff (Reader Int BReader Bool BVoid) a) may access two
extensible effects and demonstrate its use. (The complete code environments: one of type Int and one of type Bool.
Eff.hs accompanies the paper.) The library is implemented on top As a small example, consider the computation t1 below2 :
of a tiny core described in §3.4 and is designed to look like the t1 :: Member (Reader Int) r ⇒ Eff r Int
MTL [24, Chap. 18] familiar to every Haskell programmer. This t1 = do v ← ask
MTL-like interface is presented in Fig. 1. The two main points of return (v + 1:: Int )
interest are that (i) all effectful computations are represented by a Given the type annotations on the otherwise polymorphic numeric
monad Eff r, and that (ii) the type parameter r is an open union of constant, the inferred type of t1 indicates that t1 returns an Int
individual effects whose components must be Typeable. This r may after potentially manipulating an environment containing an Int.
intuitively be thought of as the set of effects that computations may The fact that r includes, at least, the Reader Int effect is expressed
perform. We shall see later that effects are represented by requests, by the constraint (Member (Reader Int) r). The operation ask
hence the name r.
There are two basic ways of indicating that an effect m is part of 2 Recall that numeric literals are polymorphic. A type annotation indicates
this open union r.1 The first is via a type constraint (Member m r); that we are asking specifically the Reader Int effect layer, out of many
possible Reader layers. Instead of annotating 1 we could have annotated
1 There are more advanced ways like MemberU2 t (t m) r described below the binding v. The annotations can be avoided altogether, at the expense of
in more detail. flexibility, see §4.
(from the Reader monad) inquires about the current value in the Whereas ter1 preserves the state accumulated at the point of ex-
environment and t1 returns the incremented value. ception, ter2 discards it; the first semantics models the conven-
We can execute the computation t1 by providing it with an ini- tional “hard-wired” interactions of state and exceptions in many
tial value for the environment, e.g., runReader t1 (10 :: Int). The languages (e.g., Scheme, ML, Java, etc.) while the second seman-
inferred type for this expression is (Eff r Int) without further con- tics models a situation in which exceptions indicate failed “trans-
straints: the Reader Int effect has been handled and hence ‘sub- actions” and hence requiring the state to snap back to its original
tracted’ from the open union. In this particular case, there are no value.
remaining effects, and the pure computation can be run to produce Here is another example of multiple effects: a higher-order
a plain Int. In contrast, run t1 gives a type error. function that applies an effectful function f to each element of a
t1r = run $ runReader t1 (10:: Int ) list, printing the debugging trace:
−− 11 mapMdebug:: (Show a, Member Trace r) ⇒
(a → Eff r b) → [ a] → Eff r [ b]
t1rr ’ = run t1 mapMdebug f [] = return []
−− No instance for (Member (Reader Int) Void) mapMdebug f (h:t) = do
−− arising from a use of 8 t1’ trace $ ”mapMdebug: ” + + show h
The types thus constitute an effect system that ensures that all h’ ← f h
t’ ← mapMdebug f t
effects must be handled. return (h’: t’)
At the expression level, the code for the computation t1 is iden-
tical to an MTL-based implementation. At the type level, the con- add :: Monad m ⇒ m Int → m Int → m Int
straint (Member (Reader Int) r) looks quite like the MonadReader add = liftM2 (+)
constraint. In fact, we may even define the MonadReader instance
for the Eff r monad. However, Eff r is more general; as described tMd = runTrace $ runReader (mapMdebug f [1..5]) (10:: Int)
where f x = ask 8 add8 return x
above, r may contain an arbitrary number of Reader effects:3
t2 :: (Member (Reader Int) r, Member (Reader Float) r) ⇒ The inferred type shows that the Trace effect is added to whatever
Eff r Float effects f may produce; the example tMd uses f with an environment
t2 = do effect. We see the composability of effects: two independently
v1 ← ask developed pieces of code and their effect types are seamlessly used
v2 ← ask
in a context that tracks both effects.
return $ fromIntegral (v1 + (1:: Int )) + (v2 + (2:: Float ))
Our framework can be used with an existing monadic library
Each occurrence of ask obtains the value from its own environment, (either user-developed or built-in, such as IO). For example,
which it finds by type (here Int vs Float). We cannot write t2 as tl1 :: (MemberU2 Lift (Lift IO) r , Member (Reader Int) r) ⇒
it is with monad transformers: with two Reader effects, monad Eff r ()
transformers compel us to choose the order and then use the explicit tl1 = ask = \x → lift ◦ print $ (x+1:: Int)
lift. In contrast, the inferred signature for t2 indicates no ordering combines Reader and IO effects in some order, as clearly seen from
of the two Reader effects.4 The order is determined only when we the inferred type of tl1. Thus an effect of some monad m is notated
run the computation: as (Lift m) in our framework. Since arbitrary monads do not gen-
t2r = run $ runReader (runReader t2 (10:: Int )) (20:: Float ) erally compose, there may be at most one Lift effect in a given com-
−− 33.0 putation, which is what the constraint MemberU2 Lift (Lift m) r
One may swap the two occurrences of runReader to handle the ef- on lift indicates, see Fig. 1. Therefore, we could have imple-
fects in the opposite order (in this particular case, without affecting mented the debugging mapMdebug in the previous example with
the result). the (Lift IO) effect instead of Trace, replacing the trace line in
In the presence of control effects such as exceptions, the order mapMdebug with lift (print h). The new mapMdebug can be
of effect handling does matter. The computation incr below incre- used as before, combining the effects of the mapping function f
ments the Int state using the MTL-like operators get and put and with IO.
tes1 combine this state manipulation with exceptions: This briefly-described framework is also fully extensible: the
incr :: Member (State Int) r ⇒ Eff r () interface of Fig. 1 is implemented as a user library that the pro-
incr = get = put ◦ (+ (1:: Int)) grammer may extend at will. We will examine more advanced ex-
amples after we explain our approach in detail and contrast it with
tes1 :: (Member (State Int) r, Member (Exc String) r) ⇒ the limitations of monad transformers.
Eff r a
tes1 = do incr; throwError ”exc”
3. The Extensible Effects Framework
The inferred signature for tes1 shows that the two effects ‘combine’
in no particular order: to run tes1 we must handle both the State We now present an implementation of the the interface in Fig. 1
and Exc effects; we must choose which to handle first. Unlike our and explain the key design decisions. It is organized as follows:
previous example, the choice matters (as indicated by the types): §3.1 introduces the basic concepts underpinning our approach by
demonstrating a single effect, Reader Int. Then, §3.2 generalizes
ter1 :: (Either String String , Int )
ter1 = run $ runState (runError tes1 ) (1:: Int )
this approach to handle a fixed but arbitrary effect and §3.4 takes
−− (Left ”exc”,2) this one step further, generalizing the approach to handle many, ar-
bitrary effects (accomplished via open unions, described in §3.3).
ter2 :: Either String (String , Int ) The following sections demonstrate our framework with more ex-
ter2 = run $ runError (runState tes1 (1:: Int )) amples and contrast it with monad transformers in expressiveness
−− Left ”exc” and efficiency.
3 The MonadReader’s version, however, requires fewer type annotations 3.1 Reader Effect as an Interaction with a Coroutine
on polymorphic numerals, see §4 for discussion. As a soft introduction to our approach, we implement the single,
4 The order of type class constraints in a type signature is insignificant. simplest effect: obtaining a (dynamically-bound) Int value from
the environment. We view effects as arising from communication as a request without specifying the return address (since no re-
between a client and an effect handler, or authority. This client- sumption is expected). The status type for such exception-throwing
handler communication adheres to a generic client-server commu- coroutines can be expressed as:
nication model, and thus may be easily modeled as a coroutine: a data VEex w = Val w | E Bool
computation sends a request and suspends, waiting for a reply; a If we instead wish to non-deterministically choose an element from
handler waits for a request, handles what it can, and resumes the a given list, we send the request that includes the list and the return
client. We use the continuation monad to implement such corou- address expecting one element in reply:
tines:
data VEch w = Val w | ∀a. E [ a] (a → VEch w)
newtype Eff a = Eff{runEff :: ∀ w. (a → VE w) → VE w}
instance Monad Eff where Examining the status type for coroutines servicing Reader,
return x = Eff $ \k → k x choice, and Exc requests, we observe that the status always in-
m = f = Eff $ \k → runEff m (\v → runEff (f v) k) cludes the Val w alternative for normal termination and some form
of E alternative carrying a request. The request typically includes
data VE w = Val w | E (Int → VE w) the return address of the form (t → VE effect w) where t, the
ask :: Eff Int expected reply type, depends on the request and the result type,
ask = Eff (\k → E k) (VE effect w), is the status type of the coroutine. Abstracting this
approach, the general type for coroutine status is revealed:
admin :: Eff w → VE w data VE w r = Val w | E (r (VE w r))
admin (Eff m) = m Val
The type variable r :: ∗ → ∗ describes a particular request. For
runReader :: Eff w → Int → w example, the Reader requests in §3.1 instantiate r with (Reader e):
runReader m e = loop (admin m) where newtype Reader e v = Reader (e → v)
loop :: VE w → w
loop (Val x) = x It may be surprising that the ‘type’ of the effect is actually a type
loop (E k) = loop (k e) constructor of kind ∗ → ∗ , constructing the type of the request
The type Eff a is the type of computations that perform control ef- from the status type of the coroutine. However, this follows directly
fects instantiated to answer types (VE w) for polymorphic w (short from the recursive nature of the request type—an open recursive
for Value-Effect, indicating the two types that make up the signa- type. This type, described in the next section, will allow us to
ture). The answer type shows that a computation may produce a compose arbitrary effects.
value (alternative (Val w)) or send a request to read the Int envi- Using this rich type, we easily generalize our monad coroutine
ronment. This request, when resumed, continues the computation, library in §3.1 to arbitrary requests:
which then recursively produces another answer of type (VE w) newtype Eff r a = Eff{runEff :: ∀ w. (a → VE w r) → VE w r}
instance Monad (Eff r)
(or diverges). The function admin launches a coroutine with an ini-
tial continuation expecting a value, which, unless the computation send :: (∀ w. (a → VE w r) → r (VE w r)) → Eff r a
diverges, must be the ultimate result. send f = Eff $ \k → E (f k)
The handler runReader launches the coroutine and checks its
status. If the coroutine sends an answer, the result is returned. If admin :: Eff r w → VE w r
the coroutine sends a request asking for the current value of the admin (Eff m) = m Val
environment, that value e is given in reply. The operation ask sends The coroutine monad is indexed by the type of requests r that the
a request that retrieves the current value from the environment as coroutine may send. The function send dispatches these requests
follows: it obtains the current continuation (the ‘return address’) and waits for a reply. It obtains the suspension k of the current
and incorporates it into the request, constructing the Int → VE w computation (a return address of type a → VE w r), passes k to the
function that will be invoked by runReader to produce the final user-specified request builder f obtaining the request body (of the
answer. type r (VE w r)), incorporates it into the request E, and delivers it
The remaining Reader operation is local, which runs a compu- to the waiting admin. The coroutine library, along with open unions
tation in a changed environment (cf. local of the Reader monad). (see §3.3), provide the entire groundwork for our effect system: the
local :: (Int → Int ) → Eff w → Eff w rest of the code, implementing various effects (monads), may all be
local f m = do written by the user. We demonstrate two such effects below (and
e0 ← ask more in §5.4):
let e = f e0
let loop (Val x) = return x The first example monad is similar to the Identity monad: it
loop (E k) = loop (k e) describes pure computations that contain no effects and send no
loop (admin m) requests. We designate Void as the type of “no request.” This type
is not populated and thus no requests are possible:
On one hand, local must handle Reader requests, similar to
data Void v −− no constructors
runReader, and on the other, local must send Reader requests
to obtain the environment value to modify. As a result, the type of run :: Eff Void w → w
local, unlike the type of runReader, does not promise to remove run m = case admin m of Val x → x
the Reader effect5 . The function run serves as the handler for pure computations,
returning their results. The type of run indicates that no effects are
3.2 Coroutines for an Arbitrary Effect possible and no requests are expected; only pure computations can
We now extend our framework to handle other effects. For example, be run.
we may model boolean exceptions: we send the exception value The effect of reading an environment from §3.1 is reimple-
mented with the generalized coroutine library as follows:
5 Here local can easily be written in terms of runReader. In the full library, newtype Reader e v = Reader (e → v)
§3.4, the two functions have to be implemented separately because run- ask :: Eff (Reader e) e
Reader, as a full handler, forces the Reader effect layer to be the topmost ask = send Reader
so to remove it, whereas local does not.
runReader :: ∀ e w. Eff (Reader e) w → e → Eff Void w used when sending a request, and prj and decomp when handling a
runReader m e = loop (admin m) where request (as demonstrated in §3.4).
loop :: VE w (Reader e) → Eff Void w The internals of the implementation are not visible to users.
loop (Val x) = return x There are several possible implementation approaches that would
loop (E (Reader k)) = loop (k e) be indistinguishable by the user: we provide an implementation
The signature of runReader indicates that it takes a computation similar to the one for HList [15]. Here is a brief summary6 :
that may send (Reader e) requests and completely handles them. data Union r v where
The result is the pure computation with nothing left unhandled. Union :: (Functor t, Typeable1 t) ⇒ Id (t v) → Union r v
Thus defined, Eff Reader can be used exactly like the Reader newtype Id x = Id x −− for the sake of gcast1
monad defined in MTL: instance Functor (Union r) where ...
t1 :: Eff (Reader Int ) Int
t1 = ask 8 add8 return (1:: Int ) inj :: (Functor t, Typeable1 t, Member t r) ⇒
t v → Union r v
The inferred type of t1 betrays it as an effectful computation. inj x = Union (Id x)
The type checker prevents running it—the computation may send
requests, which must be handled first: prj :: (Functor t, Typeable1 t, Member t r) ⇒
t1r :: Eff Void Int Union r v → Maybe (t v)
t1r = runReader t1 10 prj (Union v) | Just (Id x) ← gcast1 v = Just x
prj = Nothing
The inferred type indicates that t1r is pure, and so run t1r is well-
typed and its evaluation produces the final result, 11. decomp :: Typeable1 t ⇒
Union (t B r ) v → Either (Union r v) (t v)
3.3 Open Unions decomp (Union v) | Just (Id x) ← gcast1 v = Right x
With our general, single-effect system, we now turn our focus decomp (Union v) = Left (Union v)
to including more effects in a single computation. Recall that, to
class Member (t :: ∗ → ∗ ) r
perform an effect r, the computation sends a request of that type to instance Member t (t B r )
the handler. The type of such computation, Eff r a, is indexed by instance Member t r ⇒ Member t (t’ B r )
the type r of possible requests. Thus a computation that performs
requests r1 and r2 may send requests of type r1 or r2. Therefore The implementation of the type Union r v is essentially Dynamic,
the request itself is a disjoint union, or sum, of r1 and r2. If a and thus the constraint Typeable1 t is added to inj and prj signa-
programmer can add new request types at will, this sum must be tures. The type parameter r, the set of requests participating in the
extensible: an open union. Our open union should be a type-indexed union, is a phantom parameter. This union implementation is not
co-product [15, 28]: projecting a value not reflected in the union directly accessible to the users: data constructor Union is not ex-
type is guaranteed to fail and thus should be statically rejected. ported. The operations inj, prj and decomp are the only way for
The open unions we designed are abstract: the user of the exten- the users to deal with the open unions. Since r is phantom, the
sible effects framework sees the following interface: type class (Member t r) indeed does not need any members; it is
type Union r :: ∗ → ∗ −− abstract a compile-time constraint with no run-time footprint. Member is
also a closed class – the two instances shown here are the entirety –
infixr 1 B and the users of open unions never make instances of the class.
data (( a :: ∗ → ∗ ) B b) The class Member here permits duplicates, which are harmless, al-
lowing nesting of handlers for the same request. A request will be
class Member (t :: ∗ → ∗ ) r
handled by the dynamically closest handler. With three more lines
inj :: (Functor t, Member t r) ⇒ t v → Union r v of code, see [15], duplicates can be disallowed.
prj :: (Functor t, Member t r) ⇒ Union r v → Maybe (t v) Since Member is a compile-time–only constraint and gcast1
decomp :: Union (t B r ) v → Either (Union r v) (t v) only needs to compare two Typeable.TypeRep elements, the
The framework employs open unions for requests (whose types running-time of inj and prj is constant (with respect to the size
have the kind ∗ → ∗ ). The open union is annotated with the set r of of the union). In contrast, the previously developed libraries of
request types that may be in this union. These sets are constructed open unions [18, 32] have linear-time projections and injections.
as follows: Void stands for the empty set, and t Br inserts t in
3.4 The Full Library of Extensible Effects
the set r. We also provide a type-level assertion (a type class with
no members) Member t r that can be used to assert that the set r We now present a full library of extensible effects. Its core is built
contains the request t without revealing the structure of r. upon the Eff monad and open unions; defining effects and their in-
The three functions inj, prj, and decompose have the follow- teractions is all done by the users. Since effects such as Reader, ex-
ing explanation. The injection, inj, takes a request of type t and ceptions, non-determinism, etc. are common, we implement them
adds it to the union r. The constraint Member t r ensures that t ourselves by way of example (see Fig. 2) and we provide a few
participates in the union. (The Functor constraint is explained in helpers for convenience.
§3.4.) The projection prj does the opposite. Given a value of type The full library is similar to the example in §3.1, extended with
Union (tBr) that may have a summand of the type t, the orthogo- open unions to support multiple effects. The open union is clearly
nal decomposition decomp determines if the value has that request indicated in the data type VE w r: the type of the sent request is
type t. If it does, it is returned. Otherwise, the union value is cast to Union r, describing the status of the handled computation.
a more restrictive Union r type without t—we have just determined We start by looking at the Reader effect. The request type
the value is not of type t. Thus decomp projects Union r into two Reader is identical to that of §3.1. Its sender, the operation to ask
orthogonal “spaces:” one for the particular type t and the other for r for the value of the current environment, now injects the request
without t. This operation sets our open unions apart from previous value into the union. This is reflected in the type of ask as compared
designs [18, 32]: we can, not only extend unions, but also shrink
them. The decomposition also distinguishes our open unions from 6 Seethe file OpenUnion1.hs in the accompanying code for the complete
the extensible polymorphic variants of OCaml. The operation inj is implementation.
with the single-effect framework. The Reader handler uses the
helper handle relay to deal with arbitrary requests. In general a
request goes from one handler to the next until the appropriate
handler is found. The pattern of analyzing a request, finding if its
Sending and receiving requests, running pure code type is known to the handler, and re-sending unknown requests is
data VE w r = Val w | E (Union r (VE w r)) so common that we incorporate it into a function handle relay. This
helper function is used throughout the library. Its variant, interpose,
admin :: Eff r w → VE w r
send :: (∀ w. (a → VE w r) → Union r (VE w r)) → Eff r a
which does not ‘shrink’ the request type upon relay, is used in
handlers that are also senders of the same request, like local.
run :: Eff Void w → w The desugared version of the Reader handler is as follows:
run m = case admin m of Val x → x
runReader :: Typeable e ⇒
Helpers to relay unrecognized requests Eff (Reader e B r ) w → e → Eff r w
handle relay :: Typeable1 t ⇒ runReader m e = loop (admin m) where
Union (t B r ) v → (v → Eff r a) → loop (Val x) = return x
(t v → Eff r a) → Eff r a loop (E u) = case decomp u of
handle relay u loop h = case decomp u of Right (Reader k) → loop (k e)
Right x → h x Left u → send (\k → fmap k u) = loop
Left u → send (\k → fmap k u) = loop
As in §3.1, the return type shows that all Reader e requests are
interpose :: (Typeable1 t, Functor t, Member t r) ⇒ fully handled; the runReader computation may have other requests,
Union r v → (v → Eff r a) → though, represented by r. The runReader handler, as before, obtains
(t v → Eff r a) → Eff r a the status of the client computation using admin and analyzes it,
interpose u loop h = case prj u of handling three possible cases:
Just x → h x
→ send (\k → fmap k u) = loop 1. If the client completed, its result is returned.
Reader effect 2. If the client sent a request, we check if it is a Reader request. If
newtype Reader e v = Reader (e → v) so, the client is resumed with the current value of the dynamic
deriving (Typeable, Functor) environment. The client may then terminate or send another
request, hence we loop.
ask :: (Typeable e, Member (Reader e) r) ⇒ Eff r e
ask = send (inj ◦ Reader) 3. If the request is not a Reader request, we re-send it. That other
request, u, must have contained the return address, the suspen-
runReader :: Typeable e ⇒ sion of the type t → VE w (Reader e Br). When re-sending
Eff (Reader e B r ) w → e → Eff r w the request, runReader obtains its own suspension k, which has
runReader m e = loop (admin m) where the type VE w (Reader e Br) → VE w’ r. We must some-
loop (Val x) = return x how compose the two suspensions, to obtain t → VE w’ r.
loop (E u) = When runReader’s handler resumes it, the runReader’s client
handle relay u loop (\(Reader k) → loop (k e))
is resumed.
local :: (Typeable e, Member (Reader e) r) ⇒ The problem is how to compose k with the suspension that must be
(e → e) → Eff r a → Eff r a
somewhere in the request u, given that we have no idea what u is
Exceptions (not even its concrete type). Recall that all requests are described
newtype Exc e v = Exc e by type constructors of kind ∗ → ∗ . The full type of the request
deriving (Functor, Typeable) is obtained by applying the type constructor to the status type of
the required computation. In our case, the full type of the unknown
throwError :: (Typeable e, Member (Exc e) r) ⇒ e → Eff r a other request u is Union r (VE w (Reader e Br)). The composi-
throwError e = send (\ → inj $ Exc e) tion with runReader’s own suspension should produce the request
of the type Union r (VE w r). The problem is solved if Union r
runError :: Typeable e ⇒ Eff (Exc e B r ) a →
Eff r (Either e a)
is a functor so that we can use fmap k u since k has exactly the
right type VE w (Reader e Br) → VE w r. The type construc-
catchError :: (Typeable e, Member (Exc e) r) ⇒ tor Union r is a functor of each request type, this demonstrates the
Eff r a → (e → Eff r a) → Eff r a need for the Functor t constraint on inj and prj functions in §3.3.
Non-determinism Since Union r is a functor, VE· r is a free monad induced by the
functor. The monad Eff r then looks like a combination of the co-
data Choose v = ∀a. Choose [a] (a → v) density monad and the free monad.
choose :: Member Choose r ⇒ [a] → Eff r a Other effects and their handlers follow the Reader pattern. The
makeChoice :: Eff (Choose B r ) a → Eff r [ a]
exception effect Exc is simpler to handle since the sender is not ex-
Tracing (for debugging) pecting to be resumed. The exception recovery catchError is quite
data Trace v = Trace String (() → v) like local; the exception handler may re-throw the exception. The
trace :: Member Trace r ⇒ String → Eff r () non-determinism effect Choose is also familiar. We take choose to
runTrace :: Eff (Trace B Void) w → IO w be a primitive operation (the sender of Choose effects), for non-
deterministically choosing a value from a given list. The familiar
Figure 2. The library of extensible effects mplus and mzero are trivially expressed in terms of choose. The
handler makeChoice for Choose effects produces a list, of the re-
sults of all successful choices. Presently the handler is simplistic,
using depth-first search, like that of the List monad. Programmer
may write their own handlers with more sophisticated search strate-
gies. A computation that sends Choose requests can be handled 4.2 Lifting from Arbitrary Monads
with a variety of handlers. A valuable and prevalent feature of monad transformers is adding
effects to arbitrary monads, either developed by the user or built-in
4. Simulating the Full MTL (such as IO, ST, STM). For example, with MTL, ReaderT Int IO
is a monad that combines IO operations with accessing an Int en-
As demonstrated in §2 and the interface described in §3.4, our vironment. IO actions in such a transformed monad have to be
framework expresses code that is commonly written with monad ‘lifted’ (prefixed with lift or liftIO), rendering such a transforma-
transformers and the MTL. We have already provided several ex- tion less than transparent and requiring systematic changes across
amples of Reader, State and exception effects. This section shows entire programs. (Furthermore, IO operations like catch for catch-
how our framework expresses two common and advantageous uses ing exceptions may be impossible to use in the transformed monad,
of MTL: adding a Reader etc. effect to an arbitrary monad (‘lift- in general.)
ing’) and ‘classes’ of MTL effects such as MonadReader. The ac- Extensible effects have exactly the same feature with the same
companying code has more examples of expressing MTL idioms. limitations. We may add Reader, State, generator, and other effects
We see once again that code written with monad transformers may to an arbitrary monad m. As with MTL, actions m a will have to
be easily translated to our framework with minimal changes. be lifted (and operations that take m a actions as arguments will
require ad hoc work-arounds or may be impossible to use—as was
4.1 Type Classes for Monadic Effects the case with monad transformers in the Lüth et al [19] approach.)
Liang et al [18] introduced type classes providing individually- Although lifting looks identical to MTL, it means a different
tailored methods as primitive operations associated with each thing. The operation lift m sends the action m for execution to
monadic effect. For example, (MonadReader e m) is a type class the Lift handler:
for monads m with the Reader effect, which access an environ- data Lift m v = ∀a. Lift (m a) (a → v)
ment of type e, providing primitive Reader operations ask and
local. MTL is based on this style of effect type classes, and lift :: (Typeable1 m, MemberU2 Lift (Lift m) r) ⇒
with good reason: the benefit is that the code that uses ask and m a → Eff r a
local may be polymorphic over the monad m, requiring only the lift m = send (inj ◦ Lift m)
(MonadReader e m) constraint. Our framework provides a similar
The handler receives the request, extracts and executes the action
flexibility. Even further, classes like MonadReader may be imple-
and sends its result back:
mented as concrete, stand-alone monads by hiding the Open Union
(and thus flexibility). runLift :: (Monad m, Typeable1 m) ⇒
The type class (MonadReader e m) has a functional depen- Eff (Lift m B Void) w → m w
dency constraining m to uniquely determine the type of the en- runLift m = loop (admin m) where
vironment e. A MonadReader may have only one layer of Reader loop (Val x) = return x
effects. The advantage is that fewer type annotations are needed. loop (E u) = case prj u of
For example, in the expression local (+1) (liftM (+2) ask) both Just (Lift m k) → m = loop ◦ k
−− Nothing cannot occur
ask and local refer to the same monad. In MTL, these expressions
refer to the same environment (here a numeric type). Thus in our approach, the effects of a monad m are treated just as
Our interface for ask and local (see Fig. 1), however, does not any other effects in our framework. There is an important difference
restrict the number of Reader effects. Therefore local (+1) and though: since arbitrary monads generally do not compose there
ask in the above code may refer to numerals of different types (one may be at most one Lift layer. The type of runLift makes it a
Int and another Float, for example). If we intend the two operations “terminal handler” for computations that have Lift m and no other
to deal with the same environment, we must add annotations (e.g., effects. The constraint MemberU2 Lift (Lift m) r on lift ensures
annotating both 1 and 2 in the above code to be Int). In short the uniqueness of the Lift layer. We saw the examples of lifting
programs, we must generally annotate all polymorphic literals such from the IO monad in §2.
as numerals (long programs usually have enough context to infer
the correct type).
Thus MonadReader in MTL is less expressive (insisting on 4.3 Efficiency
a single Reader effect for a given monad) but more convenient Each layer of monad transformers adds overhead. For example,
(requiring few annotations). The interface in Fig. 1 makes the a built-with-MTL monad ReaderT Int (StateT Float) Identity
opposite trade-off. Still, extensible effects can implement the less that combines Int environment and Float state has the type, after
general but more convenient MonadReader interface (and similar desugaring, Int → (Float → (Identity a, Float)). Therefore
MonadState and MonadError): simply import our Eff library and each return must build two closures and each bind has to apply
define the Eff r monad as an instance of MonadReader. them. Exchanging the order of the two layers changes the type, yet
import qualified Eff as E the overhead of two closures remains. Chances are, however, that
instance (MemberU Reader (Reader e) r, Typeable e) ⇒ only a small part of the overall computation accesses the environ-
MonadReader e (Eff r) where ment or the state. Nevertheless, the entire computation has to pay
ask = E.ask the overhead of building and applying closures. Everyone pays for
local = E.local
the needs of the few. More transform layers further increase this
The constraint MemberU Reader (Reader e) r requires that the overhead. Our framework of extensible effects propagates requests
open union of effects r must have the unique Reader effect with for effects through a chain of handlers. The overhead depends on
environment type e. The user of our framework thus has the choice the number of other handlers between the requester and its han-
of which of the two Reader interfaces to use. The file ExtMTL.hs dler, but not on the total number of handlers. The benchmarks
in the accompanying code demonstrates many examples of this Benchmarks.hs confirm that adding more handlers increases the
approach to MTL monad classes in our framework. They have overhead in the worst case, or does not affect the performance in
exactly the same look and feel as their MTL counterparts, and the best case. With MTL, adding more layers always increases
fewer annotations are required than our examples in Eff.hs. overhead (and the increase is larger than that in our framework).
5. Beyond the MTL If we expand the type abbreviations and elide Identity, we obtain
So far it may appear that our framework is just a version of MTL [Either TooBig a] – the type of non-deterministic computations
with more convenient lifting. Our framework goes beyond MTL, in which each choice can either produce a value or the TooBig
as we shall see in this section. It begins by showing where MTL exception. The exception is hence confined to a non-deterministic
falls short. Although these limitations are rarely talked about, they choice. Our example calls for the exception to abandon the com-
are very real: common programming patterns exhibiting interleav- putation completely. We should use then the opposite order of
ing of effects are sometimes complex and inefficient (§5.1), or monad layers, ListT (ErrorT TooBig Identity) a, which desug-
even impossible to express (§5.2) when using monad transformers. ars to Either TooBig [a] – the type of computations that either
Our framework deals with these situations in an efficient, straight- produce the TooBig exceptions or the list of non-deterministic
forward way. choices. This order of monad transformers arises from the com-
position of the run functions of Identity, ErrorT and ListT in that
5.1 Inflexible Semantics of Monad Transformers order:
ex2 1 = runIdentity ◦ runErrorT ◦ runListT $ ex2 (choose [5,7,1])
This section demonstrates a simple example of raising and handling
−− Left (TooBig 7)
a single exception that, surprisingly, can only be implemented with
two ErrorT monad transformer layers, imposing extra run-time The result, shown in the comments, is as desired.
overhead on the entire computation. The second part of our example calls for catching the exception
In the absence of other effects, an exception aborts all interme- and recovering from it in some cases:
diate computations up to the dynamically closest handler. The situ- exRec :: MonadError TooBig m ⇒ m Int → m Int
ation is however more subtle when exceptions are used in the pres- exRec m = catchError m handler
ence of other effects. As illustrated in §2, there are two reasonable where handler (TooBig n) | n ≤ 7 = return n
handler e = throwError e
semantics for the combination of exceptions and state: either the as-
signments are maintained during the error handling or not. An even This wrapper checks if the argument computation ends in a TooBig
more subtle situation occurs in the presence of non-deterministic exception, but the value was not really too large. If so, we recover;
computations: should the exceptions be confined to each branch otherwise, the exception is re-thrown. Adding this wrapper to the
of the non-deterministic computation or can an exception be used previous example ex2 1
to abort an entire collection of non-deterministic choices? We will ex2r 1 = runIdentity ◦ runErrorT ◦ runListT $
study this latter situation in detail. exRec (ex2 (choose [5,7,1]))
Concretely, we consider a computation of type m Int for some −− Right [7]
arbitrary monad m and add an exception effect as follows: if the gives a surprising and undesirable result. Our intention was to re-
underlying computation returns a value greater than 5, an exception cover from the exception! The computation choose [5,7,1] makes
is thrown. The intended semantics is that the exception aborts the three non-deterministic choices: the first ex2 (return 5) throws
rest of the m-computation. This will already prove non-trivial but no exceptions, and so the overall result is Right [5]; in the sec-
possible. The next step will then be to add a handler that catches ond choice, exRec (ex2 (return 7)), an exception is thrown but it
the exception, analyzes it, and either recovers by resuming the is caught, so we expect Right [7]; the last choice gives Right [1].
normal control flow or re-throwing the exception. Hopefully we can Collecting the choices, we expected the result to be Right [5,7,1].
implement both the exception throwing and handling generically, That is, we expected the choice operator to be lifted ‘up’. Obvi-
assuming as little as possible about the monad m. ously, that is not what happened: the exception is recovered from,
The first part of the example is implemented in MTL in the but all other choices got lost.
straightforward way The reason for our failure is that part 1 of the example needs
newtype TooBig = TooBig Int deriving (Show) such order of ErrorT and ListT layers so that the exception aban-
ex2 :: MonadError TooBig m ⇒ m Int → m Int dons non-deterministic choices. When we recover from the excep-
ex2 m = do tion, the choices have already been irrecoverably lost. For the re-
v ←m covery to act as if the exception never happened, preserving the
if v > 5 then throwError (TooBig v) choices, the opposite order of ErrorT and ListT layers is needed.
else return v A single program needs two different orders of monad transform
imposing no structure on the monad m other than it must support layers.
throwing TooBig exceptions. We test the guard ex2 by applying it The example can be implemented with MTL, rather counter-
to a non-deterministic computation of choosing an integer from the intuitively, using the type:
list, where: ErrorT TooBig (ListT (ErrorT TooBig Identity ))
choose :: MonadPlus m ⇒ [a] → m a
choose = msum ◦ map return with two ErrorT layers. (See transf.hs in the accompanying
code for details). The outer ErrorT TooBig corresponds to an ex-
To run the test ex2 (choose [5,7,1]) we have to pick the monad m ception confined within a non-deterministic choice, where it does
that satisfies the MonadError and MonadPlus constraints. MTL not affect other choices and can be recovered from. If the exception
lets us build such a monad by composing monad transformers each is not recovered at the end, it is re-thrown to the inner ErrorT layer,
responsible for an individual effect. In our case, by applying layers which abandons all the choices. The following function does the
ErrorT TooBig and ListT to a base monad, Identity 7 . re-throwing:
The layers ErrorT and ListT can be composed in two dif-
runErrorRelay :: MonadError e m ⇒ ErrorT e m a → m a
ferent orders, with different behaviors. One composition order runErrorRelay m = runErrorT m = check
gives the computation type (ErrorT TooBig (ListT Identity)) a. where check (Right x) = return x
check (Left e) = throwError e
7 We will be using ListT for non-determinism even though it is not strictly
speaking a monad transformer since ListT m is a monad only when m is That is not the end of our trouble: the following code
a commutative monad. However, ListT is simple and is part of the MTL ex22 1 = runIdentity ◦ runErrorT ◦ runListT ◦ runErrorRelay $
canon, and for the purposes of our examples, ListT is a good enough ex2 (choose [5,7,1])
approximation of a monad transformer. −− Right [5]
produces the answer that is very hard to account for. The surprising ishes (returns Done) or suspends on yield (returning a continuation
behavior is a consequence of the interaction of transformer layers that can be used to resume it).
in MTL (ErrorT is also declared to be MonadPlus), which is hard- Consider a scenario of a coroutine that does pretty-printing and
wired into MTL and cannot be changed. To prevent the unwanted may inquire the dynamic environment for the current paper width.
interaction of exceptions and non-determinism that is built into The query should give the latest value set by the coroutine, or
MTL, we have to re-write ex2 to use explicit lifting the latest value set by the parent if the coroutine has not bound
ex1 :: Monad m ⇒ m Int → ErrorT TooBig m Int that parameter. The dynamic environment therefore is only partly
ex1 m = do shared between the coroutine and its parent: it starts off shared
v ← lift m but becomes coroutine-private when the coroutine alters it. The
if v > 5 then throwError (TooBig v) following characteristic example illustrates such an inherited and
else return v
private access to the dynamic environment:
and arrange to bypass the inner ErrorT TooBig layer. Finally we th3 :: MonadReader Int m ⇒ CoT Int m ()
achieve our goal: throwing an exception that terminates the (non- th3 = ay  ay  local (+10) (ay  ay)
deterministic) computation, and fully recovering from it. where ay = ask = yield
ex4 1 = runIdentity ◦ runErrorT ◦ runListT ◦ runErrorRelay $ The computation ay queries the dynamic environment and yields
ex1 (choose [5,7,1]) the result; th3 does this operation twice using the current dynamic
−− Left (TooBig 7) environment that is inherited from the parent. It then changes envi-
ex4 21 = runIdentity ◦ runErrorT ◦ runListT ◦ runErrorRelay $ ronment, and queries and yields it twice more. As the type of th3
exRec (ex1 (choose [5,7,1])) indicates, CoT layer is over the MonadReader layer.
−− Right [5,7,1] The parent thread:
The inevitable duplication of the ErrorT layers is not only inele- c31 = runReaderT (loop = runC th3) 10 where
loop (Y x k) = liftIO (print x)  local (+1) (k ()) = loop
gant but also inefficient. The computation to guard, choose [5,7,1],
loop Done = liftIO (print ”Done”)
has the type ListT (ErrorT TooBig) Int or Either TooBig [Int].
It uses no exceptions yet each use of return x, which is Right [x], starts the coroutine in an environment with the current value 10,
has to tack on the Right constructor and each bind has to pattern- prints whatever the coroutine yields, and resumes the coroutine in
match on it. The entire computation pays for something used only the changed environment, with the current value 11. The printed
at the very end. result is disconcerting: 10 11 21 11 ”Done”. It starts well: the
We have seen that the simple example of exception raising and coroutine reads and yields the current environment, which is ini-
recovery has a surprisingly complex and inefficient implementa- tially 10. The parent changed that value to 11 during the dynamic
tion because the order of monad transformer layers matters, and scope of resuming the coroutine, and the coroutine noticed that.
different parts of the same computation may require a different or- The coroutine changed the environment to 21, and the result shows
der of the layers. Luckily, we solved the conundrum by duplicating it. The last printed number is incorrect: the local changes have been
layers in this example, paying for that in efficiency. The next exam- lost after the suspension. We failed at maintaining the coroutine-
ple shows that the desired effectful computation cannot be imple- local dynamic environment.
mented at all with monad transformers. The failure is expected from the types: th3 executed in the
monad CoT Int (ReaderT Int IO) a. Its type expands to:
5.2 Interleaving Effects (a → (Int → IO w)) → (Int → IO w)
The next example illustrates the limitation of monad transformers where w is (Y Int (ReaderT Int IO), the answer-type. The sus-
forcefully: the example cannot be expressed at all using them. Two pension has the type () → (Int → IO w). Therefore, when the
effects, dynamic bindings and coroutines, interleave and no stati- parent resumes it, it has the ability to pass its own dynamic environ-
cally fixed layering suffices. The example abstracts practical cases8 ment. The type also shows that the suspension does not close over
as explored in a previous paper by a subset of the authors [16]. the dynamic environment, always accepting the dynamic environ-
The gist of the example is the dynamic environment that starts off ment from the suspension’s caller. A private dynamic environment
shared between the coroutine and its parent; when the coroutine is not possible then.
changes it, the dynamic environment becomes thread-local. With the opposite order of layers:
Dynamic binding is a different name for the environment th4 :: Monad m ⇒ ReaderT Int (CoT Int m) ()
monad, or the Reader effect, for which MTL provides the monad th4 = ay  ay  local (+10) (ay  ay)
transformer ReaderT. MTL does not provide the transformer for where ay = ask = lift ◦ yield
coroutines, but it is trivial to implement in terms of the continuation c4 = loop = runC (runReaderT th4 10)
monad transformer ContT: where loop (Y x k) = liftIO (print x)  (k ()) = loop
type CoT a m = ContT (Y m a) m loop Done = liftIO (print ”Done”)
data Y m a = Done | Y a (() → m (Y m a)) the printed result 10 10 20 20 ”Done” shows that the dynamic
environment does become private upon the local rebinding to 20.
yield :: Monad m ⇒ a → CoT a m () However, the parent can no longer affect the coroutine by changing
runC :: Monad m ⇒ CoT a m b → m (Y m a)
its dynamic environment: in fact, attempting to resume in a differ-
CoT a is the monad transformer for coroutines yield-ing the values ent environment, local (+1) (k ()) does not type check. Again,
of type a:9 The operation runC executes a coroutine that either fin- the types indicate what is going on. Now th4 has the monad type
ReaderT Int (CoT Int IO), which is Int → (a → IO w) → IO w
8 http://lambda-the-ultimate.org/node/1396#comment-16128 where w is (Y Int IO). The suspension has the type () → IO w,
http://keepworkingworkerbee.blogspot.com/2005/08/ closing over the dynamic environment of the coroutine. The sus-
i-learned-today-that-plt-scheme.html pension no longer accepts any environment from the parent.
9 For simplicity, the resumption from yield, and hence yield’s result type is With monad transformers, the same effects cannot interact dif-
(). It is easy to generalize to full coroutines that not only yield values but ferently in different parts of the same computation. The static layer-
also accept values on resumption. ing of monadic effects lets us implement either the shared dynamic
environment or the private one; we cannot express (practically sig- The Yield effect models computations that yield values of type a
nificant) programs that need both types of interaction. and that are resumed with the value of type (). Following the stan-
dard pattern, (of Reader requests) we define a new request Yield.
5.3 Overcoming Limitations of Monad Transformers The request carries the value to yield and the ‘return address’
As a demonstration of extensible effects, we re-implement the (which is a function with argument type () since we expect only
problematic examples from above, and see their behavior is ex- the () reply). The action yield sends the request. Its inferred signa-
pected and no longer puzzling. Re-implementation is a strong ture tells that the monad Eff r has to include the Yield a effect. The
word: switching from monad transformers to extensible effects interpreter for Yield requests straightforwardly produces the status
results in little or no changes in the code. The changes are mostly of coroutine, defined to be Y r a.
confined to the type signatures. It took a few lines of code to define a new effect. Since the
coroutine library is essentially the same as the one we implemented
Exceptions. The first problem in §5.1 was to guard a computation with MTL earlier, the running example of querying the dynamic
m Int: check the produced value and throw throwing an exception environment and yielding the result maintains the same form:
if the value is above some threshold. The code below is identical to th3 :: (Member (Yield Int) r , Member (Reader Int) r) ⇒ Eff r ()
the code ex2 from §5.1. th3 = ay  ay  local (+(10:: Int)) (ay  ay)
ex2 :: Member (Exc TooBig) r ⇒ Eff r Int → Eff r Int where ay = ask = yield’
ex2 m = do yield ’ x = yield (x:: Int )
v ←m
if v > 5 then throwError (TooBig v) The only difference from the MTL code is the type signature and a
else return v couple of annotations that fix the value of the dynamic environment
Only the signature differs, with the Member (Exc TooBig) r con- to be Int. (These annotations can be dropped as described in §4.)
straint instead of MonadError TooBig m. The first part of the ex- The code that runs th3 coroutine, prints the yielded value and
ample, testing that ex2 (choose [5,7,1]) raises the TooBig excep- resumes in a modified dynamic environment is also the same as
tion discarding non-deterministic choices, behaves as in §5.1; before modulo a few annotations:
runErrBig :: Eff (Exc TooBig B r ) a → Eff r (Either TooBig a) c31 = runTrace $ runReader (loop = runC th3) (10:: Int )
runErrBig m = runError m where loop (Y x k) = trace (show (x:: Int )) 
local (+(1:: Int)) (k ()) = loop
ex2 1 = run ◦ runErrBig ◦ makeChoice $ ex2 (choose [5,7,1]) loop Done = trace ”Done”
−− Left (TooBig 7) The result is 10 11 21 21 Done—which is in stark contrast with
Unlike §5.1, the same ex2 supports the second part of the exam- anything we could achieve with MTL. The result shows the corou-
ple: recovering from the TooBig exception if the exceptional value tine shares the dynamic environment with its parent; however, when
was not really too big. There are no longer failures or surprises. the environment is locally rebound, it becomes private to the corou-
The recovery code exRec is identical to the eponymous monad- tine and therefore no longer affected by the parent (that’s why the
transformer code in §5.1; only signatures differ. last two numbers in the trace are the same). The library of exten-
exRec :: Member (Exc TooBig) r ⇒ Eff r Int → Eff r Int sible effects has managed to express the important programming
exRec m = catchError m handler pattern—a task at which the MTL failed.
where handler (TooBig n) | n ≤ 7 = return n We have thus seen that our framework of extensible effects
handler e = throwError e subsumes MTL. We can express any MTL computations with our
The recovery behaves exactly as it was supposed to: framework (in essentially the same syntax), and we can also write
ex2r 1 = run ◦ runErrBig ◦ makeChoice $ code that is unwieldy or impossible with monad transformers.
exRec (ex2 (choose [5,7,1]))
−− Right [5,7,1] 5.4 Non-Determinism with Control
ex2r 2 = run ◦ runErrBig ◦ makeChoice $
exRec (ex2 (choose [5,7,11,1])) We close this section with a final example that shows that it is
−− Left (TooBig 11) much more straightforward to reason about effects in our library
than the corresponding effects in the MTL. In a classic paper,
If the exception is truly recovered from, see ex2r 1, the compu-
Hinze [9] shows how to use simple reasoning principles to derive
tation proceeds as if no exception happened, with no effect on
monad transformers. When Hinze considered backtracking with
non-deterministic choices—unlike what we have seen with monad
the Prolog-like ‘cut’, the derivations of the two monad transformer
transformers in §5.1.
implementations fail to follow the suggested reasoning principles.
Delimited Dynamic Scope. The second example in §5.2 was In particular, the term-based monad transformer is not based on
about dynamic binding in coroutines. First we need to define a free algebra; the context-passing implementation has to pattern-
new extensible effect, coroutine, in our framework. This can be match on the context and hence cannot be in a continuation-passing
done with little effort: style. Mainly, the derivation is far from being simple or mechanical,
data Yield a v = Yield a (() → v) relying on “mind-boggling” types and requiring properties that
deriving (Typeable, Functor) have not been proven (the paper hints on proofs by induction;
however the induction does not apply to recursive Haskell terms,
yield :: (Typeable a, Member (Yield a) r) ⇒ which are not well-founded). It is instructive to compare Hinze’s
a → Eff r () derivation with the remarkably straightforward implementation of
yield x = send (inj ◦ Yield x) the same example below.
The goal is to extend the monad of backtracking (with opera-
data Y r a = Done | Y a (() → Eff r (Y r a))
tions mzero and mplus) with ‘cut’ and ‘call’ whose specification [9,
runC :: Typeable a ⇒ Eff (Yield a B r ) w → Eff r (Y r a) Sec. 5] we repeat below for reference. Hinze recommends imple-
runC m = loop (admin m) where menting cut in terms of a primitive cutfalse :: m a, which is like
loop (Val x) = return Done mzero in that it discards further choices. The primitive expresses a
loop (E u) = handle relay u loop $ common Prolog idiom and has a clearer semantics with the follow-
\(Yield x k) → return (Y x (loop ◦ k)) ing equational laws:
cutfalse = k ≡ cutfalse Monad transformers and abstractions. One particular problem
cutfalse 8 mplus8 m ≡ cutfalse with monads composed of many transformers is the difficulty of
That is, cutfalse is the left zero of both (= ) and mplus. The oper- accessing a particular transformer layer: implicit lifting may be
ation call :: m a → m a delimits the effect of cutfalse: call m impossible (e.g., when there are several State layers) and explicit
executes m; if cutfalse is encountered, only choices made since the lifting is too painful. Schrijvers and Oliveira [27] cleverly address
execution of m are discarded. Hinze postulates the axioms of call: the lifting problem with a new, convenient layer access mechanism
while Swamy et al., [31] provide a system for ML that infers where
call mzero ≡ mzero
and how to lift operations. We sidestep the entire lifting problem
call (return a 8 mplus8 m) ≡ return a 8 mplus8 call m
through type-indexed effects. Our approach also solves the problem
call (m 8 mplus8 cutfalse ) ≡ call m
of indicating which of several State or Reader, etc., effects to
call (lift m = k) ≡ lift m = (call ◦ k)
perform. Since State effects are indexed by the type of the state,
The fundamental problem that so complicates the derivation of the state types naturally and automatically distinguish the effects,
monad transformers is the absence of the axiom specifying the without any extra mechanisms. Multiple State layers of the same
interaction of call with (= ) and the way to simplify nested type can be handled via a standard newtype trick:
invocations of call. newtype SInt = D Int deriving (Typeable, Num, Eq)
The framework of extensible effects does not encounter such
difficulties. First we notice that the property cutfalse being the left incr :: (Typeable a, Num a, Member (State a) r) ⇒ Eff r a
zero of (= ) tells that cutfalse is an exception: incr = do x ← get; put (x+1); return (x+1)
data CutFalse = CutFalse deriving Typeable
cutfalse = throwError CutFalse doubleIncr = do x ← incr ; y ← incr ; return (x:: Int , y:: SInt)

Since call delimits the effect of cutfalse, it is supposed to com- run $ runState (runState doubleIncr (0:: Int )) (5:: SInt)
pletely handle the CutFalse exception. As for the Choose effect, −− (((1,6),1),6)
call should intercept these requests (to accumulate the choice points The two explicit type annotations Int and SInt are sufficient to
it may have to discard) and re-issue them later. The signature of call distinguish the usage of incr in both calls of doubleIncr; we can use
summarizes these properties. The complete code follows: the newtype trick to navigate our “transformer stack” implicitly.
call :: Member Choose r ⇒ Eff (Exc CutFalse B r ) a → Eff r a
call m = loop [] (admin m) where
Handlers for algebraic effects. Our approach is closely related to
loop jq (Val x) = return x 8 mplus8 next jq the recent congruence of ideas about effect handlers [1, 2, 13, 21,
loop jq (E u) = case decomp u of 25, 33]. Each of these effect handler implementations incorporates
Right (Exc CutFalse) → mzero an effect typing mechanism and decides on the subtle trade-offs of
Left u → check jq u effect processing.
Effect handlers are the generalization of exception handlers
check jq u | Just (Choose [] ) ← prj u = next jq to other effects – the insight first realized by Plotkin and Pret-
check jq u | Just (Choose [x] k) ← prj u = loop jq (k x) nar [25]. Our handle relay in Figure 2 is indeed quite like catch
check jq u | Just (Choose lst k) ← prj u = from the current Control.Exception library [20]: handle relay only
next $ map k lst +
+ jq
check jq u = send (\k → fmap k u) = loop jq
deals with requests/exceptions of a certain type, automatically
re-throwing all other requests for an ‘upstream’ handler to pro-
next [] = mzero cess. Using existentials and Typeable to implement open union is
next (h: t) = loop t h also the same. However, our requests (‘exceptions’) are resumable
rather than abortive and our open unions are typed.
Each clause corresponds to an axiom of call or cutfalse, cover-
Like Frank [21], our library implements shallow handlers: each
ing all axioms. The code clearly expresses the intuition that call
runM selects by case analysis only those effects it wishes to handle.
watches the choice points of its argument computation. When it en-
Much other work [1, 2, 13, 33] focuses on deep handlers, in which
counters a cutfalse request, it discards the remaining choice-points.
all effects of a particular kind are handled uniformly. Compared
The accompanying Eff.hs has several examples of using cutfalse
to shallow ones, deep handlers are often more efficient but less
and call, including nested occurrences of call, which present no
flexible.
problems.
Our library manages sets of effects using both type-level con-
We have demonstrated how to define, and reason about, the
straints and type-level lists; Kammar et al. [13] rely only on type-
interaction of two existing effects, Choose and Exc. We simply
class constraints. Constraints truly represent an unordered set. Us-
defined a new, joint handler, which can be used with the previously
ing constraints exclusively however requires all effect handler def-
written code with Choose effects and alongside other handlers of
initions be top-level since Haskell does not support local type class
Choose (e.g., makeChoice).
instances. Kammar et al. rely on Template Haskell to avoid much of
the inconvenience of type-class encoding and provide a pliable user
6. Related Work interface. Our library is syntactically lighter-weight and requires no
special syntax.
Effect systems have become an active research area, with several Another trade-off is allowing multiple instances of the same
approaches being pursued concurrently and sometimes indepen- effect. We permit not only both State Int and State Float in the
dently. The main approaches improve monad transformers [27], same computation but also several instances of State Int (although
encode handlers for algebraic effects [1, 25], or start with a type- the latter can be easily disabled). A request for state value or change
and-effect system [8, 17, 32]. Our system, independently developed will be handled by the closest handler of the appropriate state type.
from EDLS years ago, ended up with many similarities to the above Eff [1] uses a region-based approach where effect instances are tied
approaches, and also with several notable differences. First, our to region indices while Brady’s library for Idris [2] allows users to
framework is not a stand-alone language; rather, it is an ordinary name and nominally refer to individual instances.
Haskell library. Haskell with common extensions turns out capable
of expressing the type-and-effect system similar to those mentioned Type-level denotation of effects. Swierstra, Filinski, and others
above. (e.g. [8]) provide type-level systems for layering effects. However,
such systems lack the ability to subtract effects (which occurs when [5] D. Espinosa. Building interpreters by transforming stratified monads.
these effects have been handled in our system). Our continuation Unpublished manuscript, 1994.
encoding and union type are similar to Filinski’s, but our union [6] D. A. Espinosa. Semantic Lego. PhD thesis, Columbia University,
type is shrunk as effects are removed. New York, NY, USA, 1995. UMI Order No. GAX95-33546.
[7] A. Filinski. Representing monads. In POPL [26], pages 446–457.
7. Conclusions [8] A. Filinski. Representing layered monads. In POPL ’99, pages 175–
We have presented a new framework for extensible effects that 188, New York, NY, USA, 1999. ACM.
subsumes previous Haskell approaches in expressiveness. The main [9] R. Hinze. Deriving backtracking monad transformers. In ICFP ’00,
highlights of our system are: pages 186–197. ACM Press, 2000.
[10] J. Hughes. The design of a pretty-printing library. In First Intl. Spring
• easy combination of computations performing arbitrary effects; School on Adv. Functional Programming Techniques, pages 53–96,
the type system collects the effects from each computation into London, UK, UK, 1995. Springer-Verlag.
a larger union. [11] ICFP. ICFP ’13, 2013. ACM Press.
• effects arise from an interaction between a client and a handler; [12] M. Jaskelioff and E. Moggi. Monad transformers as monoid trans-
a handler can choose to service one effect or several possibly formers. Theor. Comp. Science, 411(51/52):4441 – 4466, 2010.
interleaving effects; [13] O. Kammar, S. Lindley, and N. Oury. Handlers in action. In ICFP
• the effects that have been completely handled are subtracted [11], page To Appear.
from the type leaving a computation with fewer effects that can [14] D. King and P. Wadler. Combining monads. In J. Launchbury
be further composed with other effects or handled. and P. Sansom, editors, Functional Programming, Glasgow 1992,
Workshops in Computing, pages 134–143. Springer London, 1993.
We have demonstrated that our framework of extensible effects
[15] O. Kiselyov, R. Lämmel, and K. Schupke. Strongly typed heteroge-
subsumes the MTL: any effectful MTL computation can be re- neous collections. In Proc. 2004 workshop on Haskell, pages 96–107,
written in our framework, in essentially the same syntax; we can New York, NY, USA, 2004. ACM.
also write code that is unwieldy or impossible using monad trans-
[16] O. Kiselyov, C.-c. Shan, and A. Sabry. Delimited dynamic binding. In
formers. Furthermore, we have gained flexible efficiency: our dy- ICFP ’06, pages 26–37. ACM Press, 2006.
namic order of layers is contained in one monad, parts of the com-
putation that don’t use a particular effect don’t pay for it (meaning [17] D. Leijen. Koka: A language with row-polymorphic effect inference.
In 1st Workshop on Higher-Order Programming with Effects (HOPE
each effect is “pay-per-use”). 2012). ACM, September 2012.
There are several natural avenues for future work:
[18] S. Liang, P. Hudak, and M. Jones. Monad transformers and modular
• Partitioning “large” monads like the IO monad into compart- interpreters. In POPL ’95, pages 333–343. ACM Press, 1995.
mentalized sections that may each be added and removed from [19] C. Lüth and N. Ghani. Composing monads using coproducts. In ICFP
computations. These partitions include IORefs, IO exceptions, ’02, pages 133–144. ACM Press, 2002.
time functions, pure reading, and reading and writing. [20] S. Marlow. An extensible dynamically-typed hierarchy of exceptions.
• Implementing ST-like effects, explicitly marking state, and pro- In Proc. 2006 Haskell Workshop, pages 96–106. ACM Press, 2006.
viding an allocation system using monadic regions to the user. [21] C. McBride. The Frank manual. https://personal.cis.strath.
ac.uk/conor.mcbride/pub/Frank/, 2012.
• Providing a delimited control interface with shift, prompt and
control, or a similar collection of operators. [22] E. Moggi. Computational lambda-calculus and monads. In LICS,
pages 14–23, Piscataway, NJ, USA, 1989. IEEE Press.
• Cleaning up the implementation by including the Eff (continu-
[23] E. Moggi. An abstract view of programming languages. Technical
ation) monad natively in Haskell (like IO and ST). We would Report ECS-LFCS-90-113, Edinburgh Univ., 1989.
then come full-circle to the Scheme and ML families of lan-
[24] B. O’Sullivan, J. Goerzen, and D. Stewart. Real world Haskell – code
guages, providing efficient native state, IO, and continuations you can believe in. O’Reilly, 2008.
and mechanisms for users derive other effects from them. The
advantage here is that we have a type system to track these user- [25] G. Plotkin and M. Pretnar. Handlers of algebraic effects. In ESOP ’09,
pages 80–94, Berlin, Heidelberg, 2009. Springer-Verlag.
defined effects.
[26] POPL. POPL ’94, 1994. ACM Press.

Acknowledgments [27] T. Schrijvers and B. C. Oliveira. Monads, zippers and views: virtualiz-
ing the monad stack. In ICFP ’11, pages 32–44, New York, NY, USA,
We are very grateful to Simon Peyton-Jones for many helpful 2011. ACM.
comments and discussions. Insightful comments and suggestions [28] M. Shields and E. Meijer. Type-indexed rows. In POPL ’01, pages
by Sam Lindley and anonymous reviewers are greatly appreciated. 261–275, London, United Kingdom, Jan. 17–19, 2001. ACM Press.
This material is based upon work supported by the National Science [29] G. L. Steele, Jr. How to compose monads. Technical report, Thinking
Foundation under Grant No. 1117635. Machines Corporation, Cambridge, Massachusetts, July 1993.
[30] G. L. Steele, Jr. Building interpreters by composing monads. In POPL
References [26], pages 472–492.
[1] A. Bauer and M. Pretnar. Programming with algebraic effects and [31] N. Swamy, N. Guts, D. Leijen, and M. Hicks. Lightweight monadic
handlers. arXiv:1203.1539 [cs.PL], 2012. programming in ML. In ICFP’11, pages 15–27, Sept. 2011.
[2] E. C. Brady. Programming and reasoning with algebraic effects and [32] W. Swierstra. Data types à la carte. J. Funct. Program., 18(4):423–
dependent types. In ICFP [11], page To Appear. 436, July 2008.
[3] R. Cartwright and M. Felleisen. Extensible denotational language [33] S. Visscher. Control.effects, 2012. URL http://github.com/
specifications. In M. Hagiya and J. C. Mitchell, editors, Theor. Aspects sjoerdvisscher/effects.
of Comp. Soft., number 789 in LNCS, pages 244–272. Springer, 1994. [34] P. Wadler. The essence of functional programming. In POPL ’92,
[4] D. Espinosa. Modular denotational semantics. Unpublished pages 1–14, New York, NY, USA, 1992. ACM.
manuscript, 1993.

You might also like