Safe Haskell | None |
---|---|
Language | Haskell2010 |
In this part, we'll take a more detailed look at this library. You don't need to know these things to use the effects. For the most part, you can also implement your own without reading this part. To learn about that check out the next part: Tutorial.T3_CustomEffects.
That being said, the details will help you understand potential compiler errors you might get. Also, implementing more complex effects does require a bit of an understanding of the internals.
Synopsis
Transformers
Monad transformers are types with the kind (* -> *) -> * -> *
that take a monad as a
parameter and add some extra functionality on top of it, resulting in a new monad. This isn't a
tutorial on them so we'll just briefly go through the basics.
Since a transformer takes a monad and produces a new monad, they can stack on top of each other
forming 'transformer stacks'. The functions that utilize a transformer t
's functionality only
work directly if t
is the topmost transformer on the stack. Since there can obviously only be
one transformer on the top, and we want to use more than one effect, we need to lift
their
functions through the stack. For example:
so if we want to use this function in a stack that has a get
:: Monad
m => StateT
s m s
ReaderT
transformer on top we need to
use lift
:
lift
get
::Monad
m =>ReaderT
r (StateT
s m) s
If you imagine we had 5 additional layers above the ReaderT
, we'd need to call lift
5 more
times. Instead we use type classes with polymorphic functions that work over any transformer
stack. Their instances are arranged in a way that automatically calls lift
as many times as
needed. For example, the getState
function in simple-effects
has the type
. Let's say our stack is
MonadEffect
(State
s) m => m s
. The reason why we can use the ReaderT
r1 (ReaderT
r2 (StateT
s IO
))getState
function is because there are two instances for
.MonadEffect
(State
s)
instanceMonadEffect
(State
s) (StateT
s m)
and
instanceMonadEffect
(State
s) m =>MonadEffect
(State
s) (ReaderT
r m)
Instance resolution first matches the second instance and sees that it requires a
from the underlying monad MonadEffect
(State
s)m
. Then it again encounters a
ReaderT
and finally it arrives at the StateT
layer that has no other requirements.
The actual implementation for the ReaderT
instance does the lift
ing while the StateT
implementation actually uses the structure of the StateT
transformer to implement the
required function.
- Note
- This is a bit of a simplification. There isn't really a
instance, but instead a more generalMonadEffect
(State
s) (ReaderT
r m)
one that works for any transformer and any effect. It's anMonadEffect
e (t m)OVERLAPPABLE
instance so more specific ones can be chosen if they exist (as is the case with, for example, theStateT
instance for theState
effect)
Now let's see how a function like implementStateViaStateT
works. It's type is
. Say we also have a
computation with the type implementStateViaStateT
@Int :: Monad
m => Int -> StateT
Int m a -> m a
that we want to run. If we
give that computation as the second parameter of the MonadEffect
(State
Int) n => n ()implementStateViaStateT
function then n
becomes
, but since there was also a constraint on the StateT
Int m an
type, instance
resolution must check if that type satisfies it. This then finds the
instance
instance and everything works. Then finally
the actual definition of MonadEffect
(State
s) (StateT
s m)implementStateViaStateT
kicks in and it runs the StateT
transformer,
giving it an initial state value and resulting in a monadic action that's free of the state
constraint.
If we had additional effect constraints on our initial computation, then the instance resolution
would find the matching instances for the StateT
transformer that would push the constraint
onto the underlying monad. This means that our final result wouldn't just have a Monad
constraint, but also those other ones that remain to be handled.
What simple-effects
does is provide a structured way to define new effects so that they can
automatically be lifted through monad stacks. Also, for the more complex effects where just
lift
isn't enough, it enables the writer of the effect to specify how exactly to lift their
effect. This is done through the Effect
class.
The Effect
class
The core of every effect is it's Effect
instance. Here's how the class is defined:
classEffect
(e :: (* -> *) -> *) where typeCanLift
e (t :: (* -> *) -> * -> *) ::Constraint
typeCanLift
e t =MonadTrans
t type ExtraConstraint e (m :: * -> *) :: Constraint type ExtraConstraint e m = ()liftThrough
:: (CanLift
e t,Monad
m,Monad
(t m)) => e m -> e (t m)mergeContext
::Monad
m => m (e m) -> e m
The type e
is the name of your effect and it's where you specify the functionality that your effect
provides. This will be a record of monadic functions. For example, there's how it's instantiated
for the state effect:
dataState
s m =StateMethods
{_getState
:: m s ,_setState
:: s -> m () }
Then there's the CanLift
associated type. It specifies the constraint that a transformer needs to satisfy
to be able to lift the effect through it. Usually, MonadTrans
is all you need, so that's the
default instantiation. Some more complicated effects have tighter demands.
The ExtraConstraint
associated type lets you put additional requirements on monads that support
your effect. The motivation for this extra constraint is that it allows to have parameters on
your effect (like the s
parameter in
) but still allow only one instantiation of
it. For example, you can't have a function with multiple different states. This restriction
makes type inferrence much better in some cases and doesn't seem to get in the way too much in
practice. If you do end up needing multiple states, check out the Control.Effects.Newtype
module.State
s
The liftThrough
function gets an implementation of the effect e
in the monad m
and is
required to provide an implementation in the monad t m
. Here's how it might look for the
State
effect.
liftThrough
(StateMethods
g s) =StateMethods
(lift
g) (lift
. s)
As you can see, MonadTrans
is enough in this case since the lift
function is all we need.
Finally, the mergeContext
method. Given a record of methods in a monadic context, give me a
new record of methods that somehow merges that context into it. The way it's implemented is
simpler than it seems. For example, let's see how we'd implement a function with the following
signature: f :: m (a -> m b) -> a -> m b
Since our result is monadic we're free to just bind that inner function and give it the extra parameter like this:
f mamb a = do amb <- mamb amb a
If we had more than one function inside of the context, we'd just need to select the correct one
after binding it. Here's how mergeContext
is implemented for the State
effect.
mergeContext
m =StateMethods
{_getState
= do sm <- m_getState
sm ,_setState
s = do sm <- m_setState
sm s }
If these implementations seem pretty mechanical, it's because they are. So mechanical, in fact,
that in most of the cases you don't even need to write them. Just derive the Generic
class
for your effect and you get those definitions for free. Here's the definition of the
effect:State
s
dataEffMethods
(State
s) m =StateMethods
{_getState
:: m s ,_setState
:: s -> m () } deriving (Generic
) instanceEffect
(State
s)
There are a couple of conditions though. The automatic method deriving only works for what
is called, in the terminology of this library, a simple effect. The methods of a simple effect
are functions that take arguments that don't depend on the monad m
and also have a monadic
result in that monad. For example,
is a method of a simple effect
while setState
:: s -> m ()g :: m a -> m a
isn't one because the first parameter depends on m
.
One extra condition is that there can't be any universally quantified variables in the methods,
so for example if your effect has a method h :: forall a. a -> m ()
, you can't derive
liftThrough
and mergeContext
for it automatically. This isn't because it's impossible
(or even difficult), but because you can't derive Generic
for those types. In those cases
you need to write the implementations yourself.
Finally, we have the MonadEffect
class.
The MonadEffect
class
It's defined like this
class (Effect
e,Monad
m) =>MonadEffect
e m whereeffect
:: e m
So it just says that monads that implement the effect e
need to provide an implementation of
the effect's methods that works in that monad.
As discussed in the transformers section, there's an
overlappable instance given for the MonadEffect
class. Here it is:
instance {-# OVERLAPPABLE #-} (MonadEffect
e m,Monad
(t m),CanLift
e t) =>MonadEffect
e (t m) whereeffect
=liftThrough
effect
It means that any transformer t
can implement the effect e
as long as the underlying monad
can implement it and the transformer satisfies the conditions that the effect imposes on it
(in most cases, the MonadTrans
constraint).
Transformers that finally implement the effect have specific MonadEffect
instances. For
example:
instanceMonad
m =>MonadEffect
(State
s) (StateT
s m) whereeffect
=StateMethods
get
put
- Note
- The
get
andput
functions come from the Control.Monad.Trans.State module.
Runtime implementation
Defining effects in a structured way like this has an additional benefit. It lets us use
implementations that we construct at runtime! This is achieved through the RuntimeImplemented
type. RuntimeImplemented
is a special transformer defined like this:
newtypeRuntimeImplemented
e m a =RuntimeImplemented
{getRuntimeImplemented
::ReaderT
(e m) m a }
It's a wrapper around a ReaderT
that carries around an e m
implementation.
has a RuntimeImplemented
e
instance defined like thisMonadEffect
e
instance (Effect
e,Monad
m,CanLift
e (RuntimeImplemented
e)) =>MonadEffect
e (RuntimeImplemented
e m) whereeffect
=mergeContext
$RuntimeImplemented
(liftThrough
$ask
)
Essentially, ask
gives us the record, but it's inside of a monadic context so we need
mergeContext
to get it out. This lets us implement any effect at runtime using the
implement
function:
implement
:: forall e m a. e m ->RuntimeImplemented
e m a -> m aimplement
em (RuntimeImplemented
r) =runReaderT
r em
As with implementStateViaStateT
, when we give our polymorphic monadic action as a second
parameter, instance resolution checks if
has instances for all the
effects we need. Most of them just propagate through to the inner RuntimeImplemented
e m am
monad, but the effect e
that we're implementing finds the instance above and uses that reader's environment to get the
effect's methods. All that's left is to actually run the ReaderT
by giving it the record which
we're free to construct any way we want.
- Note
- One more thing. You might notice that the definition of the state effect doesn't mention the
getState
andsetState
functions. The methods in the record are called_getState
and_setState
, with an underscore. If you think about it you'll see that to actually use one of them, we first need to get them out of the record. Since we can access the record of methods for the current monad with theeffect
function, getting the current state would look like
so to reduce duplication it's convenient to define helpers like_getState
effect
andgetState
=_getState
effect
. Even simpler, though arguably more magical, is the following top level definitionsetState
=_setState
effectStateMethods
getState
setState
=effect
When we write that at the top level, we bind the first field in the
effect
record to thegetState
name and the second field to thesetState
name. It's pretty weird because we're not in any specific monad, yet we're still somehow unpacking the record. Still,getState
, for example, happily accepts the
type signature and it just works.MonadEffect
(State
s) m => m s
That's pretty much it. In the next part we'll take a look at implementing our own effects.