```
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE IncoherentInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE PartialTypeSignatures #-}
import Prelude hiding (Read, sum)
data Handler f a f' b
= Handler { ret :: a -> Free f' b
hdlr :: f (Free f' b) -> Free f' b }
,
handle :: (Functor f, Functor f')
=> Handler f a f' b -> Free (f + f') a -> Free f' b
= fold
handle h
(ret h)-> case x of
(\x L y -> hdlr h y
R y -> Op y)
hup :: f <: g => (forall f'. Functor f' => Free (f + f') a -> Free f' b)
-> Free g a -> Free g b
= case forephism of
hup h Forephism i -> permute (from i) . mask . h . permute (to i)
mask :: Functor f => Free f a -> Free (f' + f) a
= fold Pure (Op . R)
mask
permute :: (Functor f, Functor f')
=> (f ->: f') -> Free f a -> Free f' a
= fold Pure (Op . f)
permute f
instance Functor f => Monad (Free f) where
Pure x >>= k = k x
Op f >>= k = Op (fmap (>>= k) f)
instance Functor f => Functor (Free f) where
fmap f (Pure x) = pure (f x)
fmap f (Op x) = Op (fmap (fmap f) x)
instance Functor f => Applicative (Free f) where
pure = Pure
Pure f <*> m = fmap f m
Op x <*> m = Op (fmap (<*> m) x)
data End k deriving Functor
infixr 6 +
data (f + g) a
= L (f a)
| R (g a)
deriving Functor
data f :<->: g = Iso { to :: f ->: g, from :: g ->: f }
{- which satisifies
to . from = id
from . to = id -}
sum :: (f a -> b) -> (g a -> b) -> (f + g) a -> b
sum f _ (L x) = f x
sum _ g (R x) = g x
isoRefl :: f :<->: f
= Iso id id
isoRefl
isoSym :: f :<->: g -> g :<->: f
= Iso (from i) (to i)
isoSym i
isoTrans :: f :<->: g -> g :<->: h -> f :<->: h
= Iso (to i2 . to i1) (from i1 . from i2)
isoTrans i1 i2
isoSumCong :: f :<->: f' -> g :<->: g' -> (f + g) :<->: (f' + g')
= Iso
isoSumCong i1 i2 sum (L . to i1) (R . to i2))
(sum (L . from i1) (R . from i2))
(
isoSumComm :: (f + g) :<->: (g + f)
= Iso
isoSumComm sum R L)
(sum R L)
(
isoSumAssoc :: (f + (g + h)) :<->: ((f + g) + h)
= Iso
isoSumAssoc sum (L . L) (sum (L . R) R))
(sum (sum L (R . L)) (R . R))
(
data Forephism f g
= forall f'. (Functor g, Functor f, Functor f') =>
Forephism { iso :: g :<->: (f + f') }
infixr 4 <:
class (Functor f, Functor g) => f <: g where
forephism :: Forephism f g
instance Functor f => f <: f where
= Forephism (Iso
forephism L
sum id (\(x :: End a) -> case x of)))
(
instance (Functor f, Functor g) => f <: f + g where
= Forephism isoRefl
forephism
instance (Functor f, Functor g, Functor g', f <: g')
=> f <: g + g' where
= case forephism of
forephism Forephism i -> Forephism
(isoTrans
(isoSumCong isoRefl i)
(isoTrans isoSumComm (isoSym isoSumAssoc)))
inj :: f <: g => f a -> g a
= case forephism of
inj Forephism i -> from i . L
-- And now the same, but for hofunctors
hosum :: (h1 m k -> a) -> (h2 m k -> a) -> (h1 :+ h2) m k -> a
HL x) = f x
hosum f _ (HR x) = g x
hosum _ g (
hoisoRefl :: h <-:-> h
= HOIso id id
hoisoRefl
hoisoSym :: h1 <-:-> h2 -> h2 <-:-> h1
= HOIso (hofrom i) (hoto i)
hoisoSym i
hoisoTrans :: h1 <-:-> h2 -> h2 <-:-> h3 -> h1 <-:-> h3
= HOIso (hoto i2 . hoto i1) (hofrom i1 . hofrom i2)
hoisoTrans i1 i2
hoisoSumCong :: h1 <-:-> h1' -> h2 <-:-> h2' -> (h1 :+ h2) <-:-> (h1' :+ h2')
= HOIso
hoisoSumCong i1 i2 HL . hoto i1) (HR . hoto i2))
(hosum (HL . hofrom i1) (HR . hofrom i2))
(hosum (
hoisoSumComm :: (h1 :+ h2) <-:-> (h2 :+ h1)
= HOIso
hoisoSumComm HR HL)
(hosum HR HL)
(hosum
hoisoSumAssoc :: (h1 :+ (h2 :+ h3)) <-:-> ((h1 :+ h2) :+ h3)
= HOIso
hoisoSumAssoc HL . HL) (hosum (HL . HR) HR))
(hosum (HL (HR . HL)) (HR . HR))
(hosum (hosum
-- fold for Free
fold :: Functor f => (a -> b) -> (f b -> b) -> Free f a -> b
Pure x) = gen x
fold gen _ (Op f) = alg (fmap (fold gen alg) f)
fold gen alg (
-- state
data Handler_ f a p f' b
= Handler_ { ret_ :: a -> (p -> Free f' b)
hdlr_ :: f (p -> Free f' b) -> (p -> Free f' b) }
,
handle_ :: (Functor f, Functor f')
=> Handler_ f a p f' b -> Free (f + f') a
-> p -> Free f' b
= fold
handle_ h
(ret_ h)-> case x of
(\x L x -> hdlr_ h x
R x -> \p -> Op (fmap (\m -> m p) x))
```

```
un :: Free End a -> a
Pure x) = x
un (Op f) = case f of un (
```

# Algebras of Higher-Order Effects in Haskell

# Introduction

Algebraic effects and handlers are a popular technique for modeling side effects. A key idea behind the technique is to define effects as interfaces of effectful operations such that we can program against these interfaces, and provide independent implementations of these interfaces later. In part one of this blog post we explained how to model algebraic effects in Haskell, using mostly well-known techniques from Data Types à la Carte.

But many *higher-order effects* (i.e., effects with one or more operations with one or more parameters of a computation type) cannot be represented as algebraic effects using the same techniques.
As shown in part two of this blog post, we can apply effect handlers inside of abstract syntax trees to emulate higher-order effect interfaces.
But that solution does not support modularly changing interface implementations without changing the programs that use them.

In this third and final part blog post on modeling effects in Haskell, we show how to model higher-order effects as proper interfaces of effectful operations, such that we can modularly change interface implementations, like with algebraic effects and handlers, but for higher-order effects. The general solution we present in here builds on the techniques introduced in the previous two blog posts (1, 2).

The focus of the blog post series is to explain how to model and implement effects in a typed and conceptually clear way. There are excellent Haskell libraries available on Hackage that provide more efficient and (for many purposes) more ergonomic implementations of algebraic effects or scoped effects. But (to the best of my knowledge) none that support higher-order effects in general.

The techniques in this blog post and the next are based on the POPL’23 paper on Hefty Algebras by Cas van der Rest and I. The paper uses Agda, which (similarly to other dependently typed programming languages such as Idris or Coq) relies on a static over approximation of strict positivity. Therefore our paper uses containers. Haskell does not enforce strict positivity via static checks, so we can represent higher-order effects in a way that exposes the underlying concepts in a more direct way. This blog post shows how.

### Contents

# The Problem with Higher-Order Effects

As summarized in the previous two blog posts (1, 2), higher-order effects are awkward to model using algebraic effects and handlers alone.
For example, the higher-order operations `local`

from the reader monad whose interface is summarized by the following type class:

```
class Monad m => MonadReader r m | m -> r where
ask :: m r
local :: (r -> r) -> m a -> m a
-- ^^^ computation typed parameter
```

As explained in part one of the blog post, an algebraic effect is essentially an interface comprising a set of related operations.
Programs written against such interfaces are represented as abstract syntax trees whose nodes represent effectful operations.
These syntax trees are given by the following data type, commonly known as *the free monad* where `Pure`

represents a value, and `Op`

represents an operation given by a *signature functor* `f`

:

```
data Free f a
= Pure a
| Op (f (Free f a))
```

The problem with higher-order operations is that it is not obvious how to represent the `local`

operation as a signature functor that is compatible with `Free`

.
Consider:

```
data Reader r k
= Ask (r -> k)
| forall a. Local (r -> r) (X a) (a -> k)
```

The key question is: what is `X`

here?

To match the `MonadReader`

type class interface, it should be a syntax tree with the same effects as `k`

.
The problem is that `Free`

does not support signature functors that match this type constraint!

The previous blog post showed how to work around this problem by encoding a higher-order operation as a function in a style that is less modular than plain algebraic operations.

Here we present a more general solution. To this end we will use a more general notion of functor to represent signatures.

# Higher-Order Functors and Hefty Trees

Recall that a functor in Haskell is a parameterized type `f :: * -> *`

associated with a function `fmap :: (a -> b) -> f a -> f b`

which satisfies the functor laws; i.e.:

```
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
{- which satisfies
fmap id = id
fmap (f . g) = fmap f . fmap g -}
```

Instead of plain functors, we will use *higher-order functors*; i.e.:

```
type f ->: g = forall a. f a -> g a
class (forall f. Functor (h f)) => HOFunctor h where
hmap :: (f ->: g) -> (h f ->: h g)
{- which is a natural transformation; i.e.,
hmap g . fmap f = fmap f . hmap g -}
```

We will use higher-order functors (or *hofunctors* for short) to define signatures of higher-order operations in syntax trees.
In the following type—which we dub `Hefty`

because it is `H`

igher-order `EF`

fec`TY`

—the `Return`

constructor represents a value, and `Do`

represents a higher-order operation given by a signature functor `h`

:

```
data Hefty h a
= Return a
| Do (h (Hefty h) (Hefty h a))
```

For a signature functor `h :: (* -> *) -> (* -> *)`

, the type `h m k`

encodes the syntax of a higher-order operation whose *parameter computations* (i.e., the parameters of a computation type, such as the `body`

in `local f body`

) are of type `m :: * -> *`

, and whose *continuation* (the remaining computation after the operation) is of type `k :: *`

.
So the application `h (Hefty h) (Hefty h a)`

represents an operation whose parameter computations *and* continuation are both syntax trees comprising operations described by the signature hofunctor `h`

.

Using this we can represent the `Reader`

effect as a signature hofunctor:

```
data Reader r m k
= Ask (r -> k)
| forall a. Local (r -> r) (m a) (a -> k)
deriving instance Functor (Reader r m)
instance HOFunctor (Reader r) where
Ask k) = Ask k
hmap _ (Local g m k) = Local g (f m) k hmap f (
```

Here the standalone `deriving instance`

uses Haskell’s `StandaloneDeriving`

extension to derive the functor instance you would expect for `Reader r m`

.

We can also generically lift any algebraic operation to a higher-order operation:

```
data A f (m :: * -> *) k = A (f k) deriving Functor
instance Functor f => HOFunctor (A f) where
A x) = A x hmap _ (
```

And we can provide hofunctor sums similar to plain functor sums:

```
infixr 6 :+
data (h1 :+ h2) (m :: * -> *) k = HL (h1 m k) | HR (h2 m k)
deriving Functor
instance (HOFunctor h1, HOFunctor h2) => HOFunctor (h1 :+ h2) where
HL x) = HL (hmap f x)
hmap f (HR x) = HR (hmap f x) hmap f (
```

Using these, we can represent programs involving both higher-order effects, such as `Reader`

, and algebraic operations, such as the following `Output`

effect whose signature functor, smart constructor, and handler is given below:

```
data Output x k = Out x k deriving Functor
out :: Output x <: f => x -> Free f ()
= Op (inj (Out x (Pure ())))
out x
hOut :: Functor f' => Handler (Output x) a f' (a, [x])
= Handler
hOut = \x -> pure (x, [])
{ ret = \x -> case x of
, hdlr Out y k -> fmap (\(v,ys) -> (v,y:ys)) k }
```

In order to write these programs, we will use a hofunctor subtyping type class akin to the functor subtyping type class from the previous blog post.

First, we lift the notion of *functor isomorphism* to *higher-order functor isomorphism*, and the notion of *functor forephism* to *higher-order functor forephisms*:

```
type h1 :-> h2 = forall (m :: * -> *) k. h1 m k -> h2 m k
data h1 <-:-> h2 = HOIso { hoto :: h1 :-> h2, hofrom :: h2 :-> h1 }
{- which satisifies
hoto . hofrom = id
hofrom . hoto = id -}
data HOForephism h1 h2
= forall h3. (HOFunctor h1, HOFunctor h2, HOFunctor h3) =>
HOForephism { hoiso :: h2 <-:-> (h1 :+ h3) }
```

Using these, the notion of subtyping is entirely analogous to the previous blog post:

```
infixr 4 :<
class (HOFunctor h1, HOFunctor h2) => h1 :< h2 where
hoforephism :: HOForephism h1 h2
hoinj :: h1 :< h2 => h1 m a -> h2 m a
= case hoforephism of
hoinj HOForephism i -> hofrom i . HL
```

We also implement the following instances entirely analogously to the previous blog post:

`instance HOFunctor h => h :< h`

```
where
= HOForephism (HOIso
hoforephism HL
id (\(A (x :: End a)) -> case x of))) (hosum
```

`instance (HOFunctor h1, HOFunctor h2) => h1 :< h1 :+ h2`

```
where
= HOForephism hoisoRefl hoforephism
```

```
instance (HOFunctor h1, HOFunctor h2, HOFunctor h3, h1 :< h3)
=> h1 :< h2 :+ h3
```

```
where
= case hoforephism of
hoforephism HOForephism i -> HOForephism
(hoisoTrans
(hoisoSumCong hoisoRefl i) (hoisoTrans hoisoSumComm (hoisoSym hoisoSumAssoc)))
```

We use functor subtyping to define smart constructors for `Reader`

:

```
ask :: Reader r :< h => Hefty h r
= Do (hoinj (Ask Return))
ask
local :: Reader r :< h => (r -> r) -> Hefty h a -> Hefty h a
= Do (hoinj (Local f m Return)) local f m
```

We can also generically lift smart constructors of algebraic effects to higher-order effect trees by `fold`

ing over `Free`

.

```
a :: (A f :< h, Functor f) => Free f a -> Hefty h a
= fold Return (Do . hoinj . A) a
```

However, if we use `a`

to write programs as in the `localout0`

program below, Haskell’s type inference will not be able to infer what `f`

is.

```
localout0 :: Hefty (Reader Int :+ A (Output Int) :+ A End) ()
= local (+ (1 :: Int)) (do
localout0 i :: Int) <- ask
(-- Haskell does not infer that `f` is `Output Int` here a (out i))
```

We can aid Haskell’s type inference and provide more type hints if we specialize the type of `a`

for particular effects; for example, for the `Output`

effect:

```
oa :: A (Output Int) :< h => Free (Output Int) a -> Hefty h a
= a oa
```

Using the `Monad`

instance of `Hefty`

which we omit for now but show and explain later in this blog post:

```
localout :: Hefty (Reader Int :+ A (Output Int) :+ A End) ()
= local (+ (1 :: Int)) (do
localout i :: Int) <- ask
( oa (out i))
```

This `localout`

program demonstrates how generalizing from `Free`

to `Hefty`

lets us define computations involving higher-order operations and effects as abstract syntax trees, akin to how we defined computations involving algebraic operations and effects in the first blog post.

Next, we show how this representation lets us give meaning to higher-order operations by providing composable *elaborations* that desugar higher-order operations into inline applications of effect handlers akin to the inline applications we used in the previous blog post.
Unlike the previous blog post, the elaborations we will present here are factored into an *interface* given by a *signature hofunctor*.

This factoring makes it possible to change the implementation of higher-order operations for a particular program, without modifying the program itself, and without affecting the meaning of those operations anywhere else.

# Algebras of Higher-Order Operations

The meaning of higher-order effects is given by a *hofunctor algebras*, given by the following data type:

```
data h :=> g
= HA { ha :: forall a. h g (g a) -> g a }
```

We use such algebras to fold over hefty trees:

```
hfold :: HOFunctor h
=> (forall a. a -> g a)
-> h :=> g
-> (Hefty h ->: g)
Return x) = gen x
hfold gen _ (Do x) =
hfold gen alg (fmap (hfold gen alg) (hmap (hfold gen alg) x)) ha alg (
```

This `hfold`

function folds `Hefty`

into a functor `g`

.
Later, we will also provide a *generalized fold* which lets us fold `Hefty`

into any value.
For now, this notion of algebra and fold suffices to define the meaning of higher-order effects by elaborating them into algebraic effects.

For example, the following algebra generically elaborates any algebraic effect given by a signature functor `g`

into a computation `Free f`

where `g`

is a subtype of `f`

:

```
eA :: g <: f => A g :=> Free f
= HA (\(A x) -> Op (inj x)) eA
```

We can also provide an elaboration of the higher-order `Reader`

effect into an algebraic effect `AAsk`

effect.
We first define the algebraic `AAsk`

effect in terms of the following signature functor, smart constructor, and handler:

```
data AAsk r k = AAsk (r -> k) deriving Functor
aask :: AAsk r <: f => Free f r
= Op (inj (AAsk Pure))
aask
hAAsk :: Functor f' => r -> Handler (AAsk r) a f' a
= Handler
hAAsk r = pure
{ ret = \x -> case x of AAsk k -> k r } , hdlr
```

Using these, following algebra elaborates any `Reader`

effect into a computation that has at least the `AAsk`

effect, by applying an inline handler for `aask`

to pass down a modified readable value:

```
eReader :: AAsk r <: f => Reader r :=> Free f
= HA (\x -> case x of
eReader Ask k -> aask >>= k
Local f m k -> do
<- aask
r >>= k) hup (handle (hAAsk (f r))) m
```

We can run a computation by first elaborating all higher-order effects into plain algebraic effects and then run handlers.
To do so, it is useful to compose algebras using algebra composition (`/\`

):

```
infixr 6 /\
/\) :: h1 :=> g -> h2 :=> g -> (h1 :+ h2) :=> g
(/\ a2 = HA (\x -> case x of
a1 HL x -> ha a1 x
HR y -> ha a2 y)
```

For example, the following function lets us run the `localout`

program from before:

```
run :: Hefty (Reader Int :+ A (Output Int) :+ A End) a -> (a, [Int])
= let
run x y :: Free (AAsk Int + Output Int + End) _
= hfold Pure (eReader /\ eA /\ eA) x -- first elaborate
in un (handle hOut (handle (hAAsk 41) y)) -- then handle
```

Now:

`== ((), 42) run localout `

It is also possible to assign a different meaning to the `Reader`

effect to use a `State`

effect to pass down a different local value.
The signature functor, the smart constructors, and handler for `State`

are:

```
data State s k = Put s k | Get (s -> k) deriving Functor
get :: State s <: f => Free f s
= Op (inj (Get Pure))
get
put :: State s <: f => s -> Free f ()
= Op (inj (Put s (Pure ())))
put s
hState :: Functor g => Handler_ (State s) a s g (a, s)
= Handler_
hState = \x s -> pure (x, s)
{ ret_ = \x s -> case x of
, hdlr_ Put s' k -> k s'
Get k -> k s s }
```

Here is an alternative elaboration:

```
eReader' :: State Int <: f => Reader Int :=> Free f
= HA (\x -> case x of
eReader' Ask k -> get >>= k
Local f m k -> do
i :: Int) <- get
(
put (f i)<- m
v
put i k v)
```

To evaluate a `local f m`

operation, we first `get`

the old state and then mutate it (`put (f i)`

) before evaluating `m`

.
Before evaluating the continuation, we `put`

the old state.

The `run'`

function below demonstrates how we can use this alternative `eReader'`

elaboration to change the semantics of the `local`

effect, without modifying `localout`

, and without modifying the existing `run`

function.

```
run' :: Hefty (Reader Int :+ A (Output Int) :+ A End) a -> ((a, Int), [Int])
= let
run' x y :: Free (State Int + Output Int + End) _
= hfold Pure (eReader' /\ eA /\ eA) x -- first elaborate
in un (handle hOut (handle_ hState y 41)) -- then handle
```

Now:

`== (((), 41), [42]) run' localout `

This demonstrates how algebras of higher-order operations lets us modularly change interface implementations of higher-order effects; similarly to how we can modularly change interface implementations of algebraic effects.

# Folding a Monadic Bind for Hefty Trees

The `localout`

program from earlier made use of monadic bind for sequencing `Hefty`

computations.
In part one of this blog post we showed how the monadic bind of `Free`

is given by a `fold`

over `Free`

.
It is tempting to try using `hfold`

to the same end for `Hefty`

.

However, such efforts are futile. The monadic bind we are after has type

`(>>=) :: Hefty h a -> (a -> Hefty h b) -> Hefty h b`

But the type of `hfold`

is:

```
hfold :: HOFunctor h
=> (forall a. a -> g a)
-> h :=> g
-> (Hefty h ->: g)
```

Here `->:`

is a type *preserving function* (or *natural transformation*); i.e.:

`type f ->: g = forall a. f a -> g a`

The only way we can use a `->:`

to turn a `Hefty h a`

into a `Hefty h b`

is to choose `g`

to be a constant functor; e.g.:

`newtype CHefty h a b = CHefty { unCHefty :: Hefty h a } deriving Functor`

But that will only work for hefty trees whose sub-computations are guaranteed to have the same constant return type `a`

.
This assumption does not hold in general.
To implement monadic bind for hefty trees, we need a more general recursion principle than `hfold`

.

To this end, we can define a recursion principle inspired by Yang et al.’s *functorial algebras*.
This recursion principle uses the same generator and algebra as `hfold`

does, in addition to a *base generator* function of type `(a -> b)`

and a *base algebra* of type `(h g b -> b)`

:

```
ffold :: forall h g a b.
HOFunctor h -- as hfold
=> (forall c. c -> g c) -- as hfold
-> h :=> g -- as hfold
-> (a -> b) -- base generator
-> (h g b -> b) -- base algebra
-> Hefty h a
-> b
Return x) = genb x
ffold _ _ genb _ (Do x) =
ffold gen a genb ab (-- 3.
ab -- 2.
(hmap (hfold gen a) fmap (ffold gen a genb ab) x)) -- 1. (
```

The `Return`

case uses the base generator.
In order to fold a `Do`

node into a `b`

value, `ffold`

relies on a three step process:

- Recursively call
`ffold`

on the continuation to turn`h (Hefty h) (Hefty h a)`

into`h (Hefty h) b`

. - Use the
`hfold`

to turn turn the sub-branches of signature hofunctor of type`h (Hefty h) b`

into`h g b`

. - Apply the base algebra to turn
`h g b`

into`b`

.

Using `ffold`

we can implement the `Monad`

, `Applicative`

, and `Functor`

instances of `Hefty h`

.

```
instance HOFunctor h => Monad (Hefty h) where
>>= k = ffold Return (HA Do) k Do m
m
instance HOFunctor h => Applicative (Hefty h) where
pure = Return
<*> m = ffold Return (HA Do) (flip fmap m) Do f
f
instance HOFunctor h => Functor (Hefty h) where
fmap f = ffold Return (HA Do) (pure . f) Do
```

# Conclusion

There are many more examples of higher-order effects than the simple `Local`

effect discussed in this blog post.
For example, exception catching, logic programming, effectful *λ*s, and more.

The idea that we have presented in this blog post is that we can treat higher-order operations as an interface whose implementation is generally given by elaborating the interface into effects and handlers.
Since effects and handlers can be used to implement continuations (in particular, call/cc), they can be used to represent *any monadic effect*, following Filinski.

There are other approaches to defining higher-order effects using effect handlers, such as *scoped effects*.
Whereas hefty algebras involve a layer of indirection (elaborating into algebraic effects which we then handle), scoped effect handlers can give a semantics of the subset of higher-order effects known as *scoped effects* more directly.
This suffices to give a modular and concise definition of many higher-order effects.
But not all.
In particular, effectful *λ*s is not a scoped effect.
Using hefty algebras we can define a broader class of effects in a manner that is equally modular as algebraic effects.