Effect Systems, Graded Monads, and Co

Posted on March 17, 2026

One thing that annoys me about Haskell is that effectful computations are structured by a zoo of monads which don’t compose well. You have to do various convoluted things to work around this; I do not know what a monad transformer is and I don’t particularly want to. Other languages, like Koka, have properly compositional effect systems where you can use as many different effects as you need and it’ll keep track of what effects your code has, getting you all the admissibilities on pure code that Haskell has without the hassle when you need to use effects. (Actually they have even more upside than Haskell, in principle, because they can track things like divergence as effects that’d be horrible to do with non-compositional monads.)

It turns out that these are still pretty much monads! In particular, these effect systems are “graded monads”, a modification of monads which a quick look suggests were independently originated in 2014 by both Shin-Ya Katsumata and Dominic Orchard. The idea is that instead of the traditional signature for a monad M:

return: a -> M a

join: M (M a) -> M a

you parameterize M by a monoid of “grades”, with join (& thus bind, kleisli composition, etc.) applying the monoid operation. denoting the identity as I and the operation as *, this looks like:

return: a -> M_(I) a

join: M_(x) (M_(y) a) -> M_(x * y) a.

In the case of effect systems, the monoid you use is a collection of effects with a suitable merge (set, multiset, row, list, whatever). “return” takes a value and assigns it the empty collection of effects (Koka calls this “total”), because it’s already done and can’t perform any more effects; “join” says that if your computation performs some collection of effects x and also some collection of effects y, the whole computation performs the merge of all these effects. This adds a fine-grained distinction of which effects your computation can perform while being fully compositional, working with bind and Kleisli composition, etc.

My impression is that “coeffects” are what happens when you do all this to a comonad instead of a monad:

extract: M a -> a

extend: M a -> M (M a)

which graded looks like it should become:

extract: M_(I) a -> a

extend: M_(x * y) a -> M_(x) (M_(y) a)

though I haven’t actually looked all this up. The “extract” operation is where you get the simplified phrase “coeffects track what you require from your environment” because you pull a normal a out of the “context” M a.