Some people say "effect handlers are the new monads". So I thought, before all these "Effect Handlers are like X" (for X in { Burritos, Containers, ... }) blog posts come into existence, I contribute another point-of-view. This blog post does not really explain the theory behind algebraic effects and handlers. Neither does it explain why you should use them, or how. We just look at how effect handlers work operationally, by implementing them.
This post is a bit longer. If you are in a hurry, I recommend you jump to Step 2.
I do recommend the blog post Algebraic effects for the rest of us which explores another aspect of the topic.
I spend most of my awake time doing research on effect handlers (in particular in Scala and Java). Unsurprisingly, I am happy to see that effect handlers not only catch on in research, but also programmers in industry find them interesting. In particular, some developers at Facebook seem to use them as conceptual model for aspects of React.
Programming language researchers like to cut the semantics of a programming language in little pieces. Each one should be easy to explain. Understanding the language as a whole, should be understanding the little pieces. Turns out that's already difficult with simple effects like state (that is, mutable variables, heap, etc.), exceptions, I/O (that is, files, console input, println, etc.). Like monads, algebraic effects are motivated by the fact that it is terribly hard to think about programs that use effects. But nothing of this will concern us here.
When reading about the topic, you will see different terminology: algebraic effects, effects, effect handlers, ... What's the difference really? In short: The word "effects" is very overloaded -- don't try to understand it; algebraic effects means thinking about effect operations (like "throw", or "print") as building blocks to do calculus with (as in, "algebraic" reasoning); effect handlers are a generalization of exception handlers (as also explained in this post). They are practically relevant to write modular programs. That's what we are interested in.
First of all, it should be made clear that the purpose of this post is not to provide an industry strength implementation, but to give one that's as simple as possible. The first thing we will do is to implement dynamically scoped variables.
What? Are you crazy? Lisp got dynamic scoping by accident. Isn't that a bad thing?
Yes, and no. But let's not go down this rabbit hole and instead focus on the coding. What we want to implement is the following. Given a function
def render
url = Eff.get :url
puts url
end
we want to be able to (locally) provide a context, such that url
is bound to a desired value.
For instance
Eff.with url: "http://github.com" do
render
end
Eff.with url: "http://b-studios.de" do
render
end
should print both urls. Alternatively, we could also pass the url as an additional argument to render
. However,
this way we would not only pass it to render
, but also to all methods that call render
and so forth.
That is,
def view
puts "... rendering ..."
render
end
Eff.with url: "http://github.com" do
view
end
should print
... rendering ...
http://github.com
Furthermore, it should always resolve to the closest binding. That is,
Eff.with url: "http://github.com" do
Eff.with url: "http://b-studios.de" do
render
end
end
should print "http://b-studios.de"
.
Maybe you already have an idea how we could implement this API? In fact,
this is not very difficult. We can just use a mutable field and carefully keep
track that it always contains the correct value. Eff.with
writes the value, Eff.get
reads it. That's not how we gonna do it. Instead we are going to use fibers.
What? What? What's the connection between dynamic scoping and fibers?
Bear with me, there is an immediate connection (that Daan Leijen at Microsoft Research and I explored in the new language design of the Koka language, explained here). It will also greatly help us in Step 2.
So, how do we start? First of all, we think of binding variables and looking them up as communication between separate processes (or fibers). Looking up a variable corresponds to sending a request to an outer process. The outer process can decide what to respond to the request. It determines what value the variable is bound to.
Enough talk, here is the implementation: