BranchTaken

Hemlock language insights

Intrinsic Effects

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

The 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 validate_divisor! This is why we introduced the hlt effect, but as you can see, it’s not good enough to just give assert a hlt effect. validate_divisor also needs to have a hlt effect, because no data dependencies mandate its execution. This is why we added expose/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!

Semi-convergent

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 optimizations.

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:

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.