Implementing algebraic effects through typeclass prolog

Monads are a popular way to model effects in purely functional programming languages, but they come with problems. For instance, to compose them(and compose their effects), we have to use Monad transformers. An alternative that has started to emerge is algebraic effects. Algebraic effects are composable and one could see them as a type safe incarnation of delimited continuations. We used delimited continuations to implement async and await syntax in the last post. Here, however, we will try to derive a useful usage for algebraic effects pattern through the use of typeclass metaprogramming and lots of haskell extensions.

1. Obligatory pragmas

We will be writing a lot of type level code and consequently, we will need to give the compiler a a lot of LANGUAGE pragmas.

To be able to construct a [* -> *]:

{-# LANGUAGE DataKinds #-}

To be able to express that every element of a [* -> *] is a functor:

{-# LANGUAGE TypeFamilies #-}

For more complex instance declarations:

{-# LANGUAGE UndecidableInstances #-}

To be able to express PreservesShow:

{-# LANGUAGE QuantifiedConstraints #-}

To disambiguate some types:

{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}

2. Imports

Some of the type families we are going to define are actually constraints.

import GHC.Exts (Constraint)

3. Syntax tree as fixpoint of sum of functors

One of the ideas explained in the paper [1] is that we can create an extensible syntax tree by representing each constructor with a functor, by combining constructors with sums of functors and by making this type recusive through the fixpoint operator for functors.

The fixpoint operator and an example of a constructor we might want to add to a syntax tree:

data Fix f = In (f (Fix f))

data Plus s = -- s for self. This is the point of recursion
  Plus s s
  deriving (Functor, Show)

We will reuse the idea of sums of functors to represent different effects (each functor is responsible for an effect), but instead of making the tree recursive through the fixpoint operator, we will take the free monad from the sum of functors.

The free monad Free f makes f into a monad, because a term of type Free f x is inhabited by terms of shape \( f^n(x) \) for some \( n \in \mathbb{N} \). \( f^0(x) = x \) is Return and the Join operation works becaus \( f^n(f^m(x)) = f^{n+m}(x) \).

data Free f x = Return x | Join (f (Free f x))
  deriving Functor

4. The Sum constructor

If we have a list of effects fs :: [* -> *], we would like to have a way to combine these into a new functor * -> *. This is what this type does.

data Sum (fs :: [* -> *]) (x :: *) where
  Head :: f x -> Sum (f ': fs) x
  Tail :: Sum fs x -> Sum  (f ': fs) x

A term of type Sum '[A, B, C] x is either a term of type A x, B x or C x.

5. Inclusion of functors

For type inference to work well for us, we want our "effect sets" relations to be computed by Haskell's instance resolution system. We will define a typeclass for every relation we wish to have on effect sets. We will then define instances (derivation rules) through with the Haskell compiler will generate code for us.

We first need an "is-element" relation(it is very similar to the \( \prec \) typeclass they use in [1]) which will be witnessed by a way to turn an f x into a Sum fs x whenever f :∈: fs.

class (e :: * -> *) :∈: (es :: [* -> *]) where
  inject :: e x -> Sum es x

To make the compiler automatically verify this relation, we will give it derivation rules:

An effect is an element of an effect set if it is the first element of the set.

instance {-# OVERLAPPING #-} f :∈: (f ': fs) where
  inject = Head

An effect is also an element of an effect set if it is an element of the tail of the set.

instance f :∈: fs => f :∈: (g':fs) where
  inject = Tail . inject

Here is an example of this typeclass in action. We use it to automatically "cast" a term to a "bigger" type.

data A s = A s
data B s = B s

test1 :: Sum '[A, B] Int
test1 = inject $ B 12

6. Subset relation

With the help of our membership relation, we can define a new relation which will let us express the fact an effect set is smaller than an other effect set. This relation will be witnessed by a function very similar to inject, apart for the fact it converts from a Sum.

class (l :: [* -> *]) :⊆: (r :: [* -> *]) where
  permute :: Sum l x -> Sum r x

The empty set is a subset of everything (this is the base case of our typeclass recursion).

instance '[] :⊆: fs where
  -- no constructors for Sum '[] x
  permute _ = error "impossible"

If the first element of the LHS set is a member of the RHS set and the last elements of the LHS set are a subset of the RHS, then the LHS is a subset of the RHS. (this is the induction case of our typeclass recursion)

instance (f :∈: gs, fs :⊆: gs) => (f ': fs) :⊆: gs where
  permute (Head h) = inject h
  permute (Tail t) = permute t

Here is an example of ho we might use this relation:

data C s = C s

test2 :: Sum '[B, C, A] Int
test2 = permute test1

Because of the way we set up our derivation rules, whenever all the functors in a set are known, the instance search algorithm will automatically generate the necessary code to rearange the order of a functor sum.

7. Effect type constructor

Now we ave a way to represent effects (as sum of functors), let's tie them in a circle using the free monad. The following construction is equivalent to the free monad on the sum of the effects.

data Eff (fs :: [* -> *]) (x :: *) where
  Pure :: x -> Eff fs x
  Impure :: Sum fs (Eff fs x) -> Eff fs x

-- To unwrapp empty effects into pure values
unwrapEff :: Eff '[] x -> x
unwrapEff (Impure _) = error "impossible"
unwrapEff (Pure x) = x

This construction will let us sequence many effects together. Another thing to notice is that

8. Free monoids, free monads, [x] and Eff fs

We all know the meme phrase "a monad is just a monoid in the category of endofunctors", but what does it tell us about the free monad? It is just the free monoid in the category of endofunctors.

8.1. What even is a free monoid?

A monoid is an object with an associative operation \( * \) and a neutral element \( 1 \). This means that for every \( a, b, c \):

\[ a * (b * c) = (a * b) * c \]

\[ 1 * a = a = a * 1 \]

The free monoid on an alphabet is the smallest monoid made from this alphabet. It is the smalles in the sense that it has no more equalities than absolutely necessary to make it a monoid.

The free monoid on an alphabet set is the list containing elements of this set. The neutral element \( 1 \) is the empty list and \( * \) is append.

As it turns out, we can concisely express the type of lists of \( x \) with this equation:

\[ L(x) = 1 + x \times L(x) \]

Here, \( 1 \) is the singleton set (a set with one element) which acts as the neutral element for the cartesian product \( \times \) on sets and \( + \) is the disjoint union of sets. If we expand this recursive definition, we will see that:

\[ L(x) = \sum_{n=0}^{\infty} x^n \]

where \( x^0 \) is the singleton set and \( x^n \) is the \( n \)-tuple of \( x
\)s.

8.2. Free monad

If we translate all of our operations from the set world to the endofunctor world, this is what happens:

  1. \( + \) which is the set disjoint union becomes the functor disjoint union where \( (f + g)(x) = f(x) + g(x) \).
  2. \( \times \) which was the cartesian product becomes \( \circ \) the functor composition.
  3. \( 1 \) which was the singleton set (neutral element of \( \times \)) becomes \(
   \text{id} \) (neutral element of \( \circ \)).

Finally, when translating the recursive equation for the list, we get:

\[ L(f) = \text{id} + f \circ L(f) \]

This object – the free monoid on an alphabet of endofunctors – is exactly Eff fs.

9. Show for Eff fs x

When working with effectful values, it will be useful to have Show (Eff fs x) when possible. To achieve this, we will need to define new typeclasses and some instances.

We say that a * -> * "preserves show" when Show (f x) whenever Show x.

class (forall x. Show x => Show (f x)) => PreservesShow f

We know that Sum fs preserves show if and only if al of the elements of fs preserve it too.

type family AllPreserveShow (fs :: [* -> *]) :: Constraint where
  AllPreserveShow (f ': fs) = (PreservesShow f, AllPreserveShow fs)
  AllPreserveShow '[] = ()

instance (AllPreserveShow fs, Show x) => Show (Sum fs x) where
  show (Head h) = show h
  show (Tail t) = show t

Finally, because Eff fs x uses Sum fs, it also preserves show whenever Sum fs preserves show.

instance (AllPreserveShow fs, Show x) => Show (Eff fs x) where
  show (Pure x) = show x
  show (Impure t) = show t

10. When the effects fs are functors, Eff fs is a functor

Further down, having Functor instances for everything will be very useful. This is why it is important automatically derive Functor instances whenever possible.

type family AllFunctors (fs :: [* -> *]) :: Constraint where
  AllFunctors (f ': fs) = (Functor f, AllFunctors fs)
  AllFunctors '[] = ()

Again, a sum of * -> *s is a functor if and only if they all are.

instance AllFunctors fs => Functor (Sum fs) where
  fmap f (Head h) = Head $ fmap f h
  fmap f (Tail t) = Tail $ fmap f t

By the same reasoning as with Show, Eff fs is a functor whenever fs are.

instance AllFunctors fs => Functor (Eff fs) where
  fmap f (Pure x) = Pure $ f x
  fmap f (Impure fx) = Impure $ (fmap . fmap) f fx

11. Eff fs is also a monad

The categorical presentation of a monad uses a join operator instead of >>=. Here, we will first implement effJoin (it simply removes a Pure between the two layers of Eff fs) and derive effBind with it.

effJoin :: AllFunctors fs => Eff fs (Eff fs x) -> Eff fs x
effJoin (Pure x) = x
effJoin (Impure fx) = Impure $ (fmap effJoin) fx

effBind :: AllFunctors fs => Eff fs x -> (x -> Eff fs y) -> Eff fs y
effBind fx f = effJoin $ fmap f fx

The two instances we need to have Monad (Eff fs) then flow naturally.

instance (AllFunctors fs) => Applicative (Eff fs) where
  pure = Pure
  ff <*> fx = effBind ff (\f -> fmap f fx)

instance (AllFunctors fs) => Monad (Eff fs) where
  (>>=) = effBind

12. Why do we need Monad (Eff fs)?

I hear you ask: "At the beginning, you presented algebraic effects as an alternative to monads, so why are you making Eff fs a monad?"

The reason algebraic effects are interesting is because they compose very well. Monads, in their normal form, don't. What we do here is compose the effects before turning them into a monad. This way, we only have to think about a single "monad constructor": Eff.

13. Lift a term into an effect

Now that we have built ourselves an ontology of typeclasses, functors and monads, we need utilities to actually use the algebraic effects. The first one of them will be this simple effInj function which will let us turn a naked effect into a fully-clothed effectful value.

effInj :: (Functor f, f :∈: fs) => f x -> Eff fs x
effInj e = Impure $ inject $ fmap Pure e

14. Example

Here is how to use it. Let's say we defined two effects for asking for a string and asking for an integer. The function that will find itself inside the GetInt or the GetString will be the continuation (you can read more about continuations in My last post) to which will be passed the String or Int. Here, we use id, because the continuation of our block will automatically be add through the monadic operations.

data GetString a = GetString (String -> a)
  deriving Functor

data GetInt a = GetInt (Int -> a)
  deriving Functor

type MyEff = [GetString, GetInt]

comp :: Eff MyEff String
comp = do
  int <- effInj $ GetInt id
  str <- effInj $ GetString id
  return $ str ++ show int

15. Permuting effects

Here is an utility function to embed an effectful value into a bigger effect space.

effPerm :: forall fs gs x. (AllFunctors fs, fs :⊆: gs) => Eff fs x -> Eff gs x
effPerm (Pure x) = Pure x
effPerm (Impure sx) = Impure $ permute @fs @gs $ fmap (effPerm @fs @gs) sx

16. Handlers

The idea of a handler is what makes algebraic effects ergonomic. It lets us easily handle one of the effects using the other effects in the set.

handle :: forall fs g gs x. (Functor g, AllFunctors gs, AllFunctors fs,fs :⊆: (g ': gs))
  => (forall x. g x -> Eff gs x) -> Eff fs x -> Eff gs x
handle trans (Pure x) = (Pure x)
handle trans (Impure x) = case permute @fs @(g ': gs) x of
  Head h -> let val = fmap (handle trans) h
                val' = trans val
            in effJoin val'
  Tail t -> let val = fmap (handle @fs @g @gs trans) t
            in Impure val

You use handle a bit like you use try / catch blocks in other programming languages. Whenever the effect you want to eliminate is used, you must do so using one of the remaining effects in the set.

comp' :: Eff '[GetString] String
comp' = handle func comp
  where func :: GetInt x -> Eff '[GetString] x
        func (GetInt f) = return $ f 0

17. More advanced handler

Let's say you want an effect GenSym :: (Int -> x) -> GenSym x that generates unique integers. We need some kind of way to remember the last number we gave out. A Handler f fs lets us handle a f effect using fs and also, at the same time, give the next Handler f fs to use.

data Handler (f :: * -> *) (fs :: [* -> *]) where
  Handler :: (forall x. f x -> Eff fs (x, Handler f fs)) -> Handler f fs

Now we need a handleFold function.

handleFold :: forall fs g gs x. (Functor g, AllFunctors gs, AllFunctors fs,fs :⊆: (g ': gs))
  => Handler g gs -> Eff fs x -> Eff gs x
handleFold _ (Pure x) = Pure x
handleFold (Handler fold) (Impure x) = case permute @fs @(g ': gs) x of
  Tail t -> Impure $ fmap (handleFold (Handler fold)) t
  Head h -> do
    (val, fold') <- fold h
    handleFold fold' val

Example:

data GenSym x = GenSym (Int -> x)
  deriving Functor

syms :: Eff '[GenSym] [Int]
syms = do
  x <- effInj $ GenSym id
  y <- effInj $ GenSym id
  z <- effInj $ GenSym id
  return [x, y, z+1]

symsEvaled :: Eff '[] [Int]
symsEvaled = handleFold (fh 0) syms
  where fh :: Int -> Handler GenSym '[]
        fh k = Handler $ \(GenSym cont) -> return $ (cont k, fh (k+1))

18. References

[1]
W. Swierstra, “Data types à la carte,” J. funct. program., vol. 18, no. 4, pp. 423–436, Jul. 2008, doi: 10.1017/S0956796808006758.

Date: 2024-05-29 Wed 00:00

Author: Justin Veilleux

Created: 2025-09-25 Thu 18:11

Validate