At first glance Hemlock’s effects system looks pretty typical, but as soon as you notice that there is no support for effect handlers, you may wonder if Hemlock even has an effects system. And what is parametric mutability all about?
Hemlock’s effects/mutability system started out solely focused on how to specify interactions between mutable values and the language runtime. We wanted function signatures to comprehensively divulge their effects on mutable values, and thus the (hopefully) common case of pure function calls could be aggressively optimized without worrying about hidden effects. We had a relatively simple taxonomy of load/store effects both for the “internal” runtime environment and for the “external” operating system (OS). That seemed sufficient until we realized that halting is an effect we can’t always safely ignore.
Let’s look at an example where ignoring the
hlt (may-halt) effect is okay. Consider division,
where division by 0 halts execution.
# val assert 'a: bool >hlt-> a # val validate_divisor: uns -> unit let validate_divisor y = assert (y <> 0) # val div: uns -> uns -> uns let div x y = validate_divisor y x / y
div function signature conceals the transitive
hlt effect in
assert, but that’s totally
okay, because we don’t need to validate the inputs unless the result is actually needed by the
program. But it’s not okay that
validate_divisor conceals the
hlt effect. The result of
validate_divisor isn’t used, so the compiler can optimize out the call to
is why we introduced the
hlt effect, but as you can see, it’s not good enough to just give
validate_divisor also needs to have a
hlt effect, because no data
dependencies mandate its execution. This is why we added
conceal machinery to Hemlock (a
story with its own twists and turns).
# val validate_divisor: uns >hlt-> unit let validate_divisor y = expose hlt assert (y <> 0)
More recently we realized that a concealed-by-default
alloc (automatic runtime allocation) effect
can be used to prohibit allocation in low-level code. This means the garbage collector can safely
be written in Hemlock!
I was unaware of the literature on algebraic effects systems until relatively recently, so it was a shock to see how similar Hemlock’s effects algebra looks relative to the one proposed for OCaml, as described in this presentation by Leo White. This recent Koka workshop got me thinking about how Hemlock’s effects system looks similar in many ways, but it’s a very different beast.
As mentioned, Hemlock does not have effect handlers; instead its effects are intrinsic to
computations. In other words, there are fundamental computations at the foundation of Hemlock’s
runtime which are labeled as having e.g.
st (store) effects, but there’s no “handling” of those
effects; they simply occur as a transitive side effect of computation. The effects system is an
inviolable contract between the programmer and the compiler that informs program behavior and valid
One could rightly argue that the lack of effect handlers reduces available functionality. Indeed Hemlock eschews non-local control flow (continuations, effect handlers, exceptions, coroutines, generators, async/await, etc.) in favor of shared-nothing asynchronous actor-based parallelism. On the flip side, Hemlock’s approach to effects appears to have some advantages:
- There is no run-time overhead.
- Concealable effects protect against brittle API problems, as exemplified by Java’s checked exceptions.
- Effect prohibition (e.g. of
alloc) may enable highly constrained low-level programming.
We try really hard to base Hemlock on designs which have proven effective in other languages. But Hemlock’s effects/mutability system addresses a niche that to our knowledge has barely been explored by other languages. We’ve had the luxury of several years(!) to iterate on a design that is barely recognizable from where we started, and the resulting approach looks very promising. May the implementation bear that out.