by tome | 2025-11-09 12:26:27+00:00
I found it interesting to read this with my Haskell effect system Bluefin[1] in mind.

Bluefin has what this book calls "exception capabilities"[2] (checked ones, as I guess all exception capabilities are), although Bluefin refers to "capabilities" as "handles" (i.e. "handles to an effect"). However, no special syntax or implicitly passed arguments are required. The handle is just a normal value that you pass around as a function argument. I often get the incredulous question "but won't that become really tedious?". My experience is no, quite the opposite, it's not tedious it's in fact very convenient and straightforward to be able to specify precisely where these capabilities go without having to rely on type inference. Passing capabilities as values also allows you to take advantage of language features around function arguments, such as unused argument warnings and the converse "you should have provided this argument but you didn't" errors.

I find the idea of "linear continuations in OCaml" (where the handlers are deep) amusing. It doesn't seem widely appreciated that these are already in basically every language, and called "function calls". That is in fact the implementation of Bluefin's Coroutine[3] type (and Stream[4], which is just a specialisation of Coroutine):

newtype Coroutine a b e = MkCoroutine (a -> Eff e b)

Implementing them as such avoids all sorts of problems: special syntax, special typing rules, special support in the run time system. Now, there is one feature of Bluefin's Coroutine that requires support on the run time system, but it's so common it can hardly be described as special: threading. The function connectCoroutines[5] allows two different coroutines to "switch between stacks" simply by running them in separate threads, where as a matter of course they have separate stacks. That is, in a sense "cooperative multithreading"

"Algebraic" effects in Bluefin can be obtained by defining Coroutine handles for every element of your algebraic signature, again no special syntax, no special typing rules, no special support in the run time system. However, there is one downside: because Bluefin just piggy backs off the host run time system all the continuations are "linear" or "single shot" (or perhaps more accurately, "affine" or "at most single shot", because exceptions use the continuation zero times). This simplicity plus the paucity of examples that truly require multishot continuations makes the trade off well worth it in the practical use cases I have.

When it comes to type systems, the simplicity of passing handles as normal Haskell values means that all complications to do with subtyping and row typing are absent. Rather than a "row type" of effects we just pass in as arguments the handles for effects we want available in a function. The only innovation of Bluefin in terms of the type system is to extend the state tracking of Haskell's ST monad[6] to arbitrary collections of effects[7], not just mutable state references.

In summary, if people are interested in checking out a production ready, battle tested, simple Haskell effect systems that overcomes every challenge raised in this article (apart from the support of multishot continuations) then I recommend they check out Bluefin[1]. You're welcome to ask questions or share comments at any time on the Bluefin issue tracker[8].

[1] https://hackage-content.haskell.org/package/bluefin-0.2.0.0/...

[2] https://hackage-content.haskell.org/package/bluefin-0.2.0.0/...

[3] https://hackage-content.haskell.org/package/bluefin-0.2.0.0/...

[4] https://hackage-content.haskell.org/package/bluefin-0.2.0.0/...

[5] https://hackage-content.haskell.org/package/bluefin-0.2.0.0/...

[6] https://www.stackage.org/haddock/lts-23.24/base-4.19.2.0/Con...

[7] https://hackage-content.haskell.org/package/bluefin-0.2.0.0/...

[8] https://github.com/tomjaguarpaw/bluefin/issues/new