Monad transformers II: composition of error and state transformers
The except (error) monad transformer
In our interpreter, expressions might refer to unbound variables / references.
> runEval $ eval $ parse "q + 1" *** Exception: Variable q not found.
> runEval $ eval $ parse "!q" *** Exception: Variable q not found.
We need some exception handling mechanism into the language of expressions.
We add a data type which captures the type of exceptions (errors) occurring in our interpreter:
data Err = SegmentationFault | UnboundVariable String | OtherError String
First, we could explicitly indicate that errors might happen in the interpreter
eval :: Expr -> Either Err Value
This means that there would be some plumbing to check if every recursive call has finished succesfully, and if it is not the case, fail the whole computation.
Instead we use a monad transformer!
ExceptTtakes a monadmand returns a monad which support two operations:throwErrorandcatchError— such monads are calledMonadError.A monad obtained by the transformer `ExceptT` is a `MonadError`!type ExceptT e m a runExceptT :: ExceptT e m a -> m (Either e a) throwError :: MonadError e m => e -> m a catchError :: MonadError e m => m a -> (e -> m a) -> m a
Composition of effects
We add an error monad (called exception monad in GHC) to our evaluation monad
Eval.We can understand the interaction between the state monad and the error monad by looking at their implementations (i.e. types).
It matters whether we stick the error monad on the outside or the inside of the state monad.
With
ExceptTon the outside, we will represent computations as
We have computations of type
ExceptT err (State s) a
which is isomorphic to:
State s (Either err a)
which is in turn isomorphic to:
s -> (Either err a, s)
No matter whether we return
Right aorLeft err, the final state is also returned.State changes will not be rolled back when there's an exception.When there is an exception, the state information is there!
Instead, if we place
ExceptTon the inside, i.e., adding a state monad on top of an error monad, computations will be represented as:
We will have computations of the type
StateT s (Except e) a
which is isomorphic to
s -> Except e (a, s)
and in turn
s -> Either e (a, s)
If a computation fails, we lose any changes to the state made by the failing computation.Observe that the state is just passed as a return value in the underlying monad.
Interpreter 3: ExceptT on the inside
We apply the monad transformer on the inside and wrap it with a reader and state monad.
newtype Eval a = Eval { unEval :: StateT Store (ReaderT Env (ExceptT Err Identity)) -- new a } deriving ( Functor, Applicative, Monad , MonadState Store , MonadReader Env , MonadError Err -- new )The run function then includes
runExceptTright after the reader's run functionrunEval :: Eval a -> Either Err a -- new type! runEval m = m & unEval & (`evalStateT` emptyStore) & (`runReaderT` emptyEnv) & runExceptT & runIdentityWe adapt the interpreter's auxiliary functions such that they can raise errors:
Looking up a variable in the environment
lookupVar :: Name -> Eval Value lookupVar x = do env <- ask case Map.lookup x env of Nothing -> throwError (UnboundVariable x) -- new ...Deferencing a location which does not exist
deref :: Ptr -> Eval Value deref p = do st <- get let h = heap st case Map.lookup p h of Nothing -> throwError SegmentationFault -- new ...We finally write our interpreter (the only new case is
Catch)eval :: Expr -> Eval Value eval (Lit n) = return n eval (a :+: b) = (+) <$> eval a <*> eval b eval (Var x) = lookupVar x eval (Let n e1 e2) = do v <- eval e1 localScope n v (eval e2) eval (NewRef e) = do v <- eval e newRef v eval (Deref e) = do r <- eval e deref r eval (pe := ve) = do p <- eval pe v <- eval ve p =: v eval (Catch e1 e2) = catchError (eval e1) (\ _err -> eval e2)We can recover from errors
testExpr2 = parse "(try !p catch 22) + 1738"
>>> runEval $ eval testExpr2 1760
What happens with the side-effects when an error occurs?
testExpr3 = parse "let one = new 1; \ \ let dummy = (try ((one := 2) + !7) catch 0); \ \ !one"What is the value of
one?Let us run it first
>>> runEval $ eval testExpr3 Right 1
The side-effects (namely,
one := 2) are reverted if an error occurs!To explain what happens in more detail, let us see the program
The expression
((one := 2) + !7)can be thought as a function which takes a store and always returns an error, i.e. a function semantically equivalent to\ s -> Left SegmentationFault.Then, due to the try-catch statement, the expression
(try ((one := 2) + !7) catch 0)catches the error and then produces a function semantically equivalent to\ s -> Right (Lit 0, s). Observe that it returns the state before the effectone := 2.
Interpreter 4: ExceptT on the outside
We apply the exceptionmonad transformer on the outside:
newtype Eval a = Eval { unEval :: ExceptT Err -- new (StateT Store (ReaderT Env Identity)) a } deriving ( Functor, Applicative, Monad , MonadState Store , MonadReader Env , MonadError Err -- new )Since
ExceptTis the outermost transformer,runExceptTis the first one to runrunEval m = m & unEval & runExceptT & (`evalStateT` emptyStore) & (`runReaderT` emptyEnv) & runIdentityThe interpreter, and the auxiliary functions remain the same.
What happens with the side-effects when an error occurs?
testExpr3 = parse "let one = new 1; \ \ let dummy = (try ((one := 2) + !7) catch 0); \ \ !one"What is the value of
one?>>> runEval $ eval testExpr3 Right 2
Observe that the program has recovered but the side-effects within the try-catch blocked were not rolled-back.
To explain what happens in more detail, let us see the program:
The expression
((one := 2) + !7)can be thought as a function which takes a store and returns an error but keeping the state at the point of the failure. You can think of the computation as a function semantically similar to\ s -> (Left SegmentationFault, s')wheres'is the same store assexcept that referenceoneis set to2.When the exception handler is executed, it takes the state from where
((one := 2 + !7))left it, i.e.s'. So, the result of!onein the last line of the program is2.
Which one is better? ExceptT outside or inside?
That is application-specific.
In our interpreter, the expected semantics for programs is obtained with
ExceptTon the inside.There are some side-effects which cannot be undone, e.g., launching a missile. For those effects,
ExceptTon the outside might be suitable.
Interpreter 5: Adding I/O
We want to add a new effect: I/O (for simplicity we only consider output).
We will add a function to print out messages:
let x = print \"hello\" ; 42
We will add the IO monad into the definition of
Evalnewtype Eval a = Eval { unEval :: StateT Store (ReaderT Env (ExceptT Err IO)) -- new IO for Identity a } deriving ( Functor, Applicative, Monad , MonadState Store , MonadReader Env , MonadError Err , MonadIO -- new )We replaced
IdentitybyIO.The type
Eval ais isomorphic to:s -> r -> Either Err (IO (a, s))
This indicates that the except (error) monad still rolls back the effects on the store, but not the ones in
IO(e.g., launch a missile).We make
Evalan instance of the classMonadIO.As a result,
Evalcan lift (cast) every IO-action into itself:liftIO :: MonadIO m => IO a -> m a
We define the function to show a string:
msg :: String -> Eval () msg = liftIO . putStrLn
We add a constructor to represent output commands:
eval :: Expr -> Eval Value eval (Lit n) = return n eval (a :+: b) = (+) <$> eval a <*> eval b eval (Var x) = lookupVar x eval (Let n e1 e2) = do v <- eval e1 localScope n v (eval e2) eval (NewRef e) = do v <- eval e newRef v eval (Deref e) = do r <- eval e deref r eval (pe := ve) = do p <- eval pe v <- eval ve p =: v eval (Catch e1 e2) = eval e1 `catchError` \ _err -> eval e2 eval (Print m) = do msg m -- new return 0The following program produces an I/O effect before an exception is thrown
"let one = new 1; \ \ let dummy = (try ( print \"hello!\" + (one := 2) + !7) \ \ catch 0); \ \ !one"
If we run it, it produces the output and then it rolls back the effects on the store (i.e.,
one := 2):>>> test3 hello! Right 1
Implementing monad transformers
So far, we have just seen how to use the monad transformers provided by GHC and the magic of
deriving.In case you designed your own monad to handle certain special effects, you might want to make a monad transformer for adding such effects to any monad!
We will develop the state, error, and reader monad transformers.
MyStateT: a state monad transformer
We start by defining a data type to host the transformed monad, i.e., the monad which the transformer considers.
data MyStateT s m a
The transformer takes the monad
mand synthesises a new monad with state (of types) which containsmunderneath.To write a monad transformer, it is usually a good principle to see the underlying monad as a black-box, i.e., you should assume nothing about its structure.
If monad
mis black-box, you can only use itsreturn,(>>=), and the non-proper morphismsImportanty, recall that monad
mis polymorphic on the produced result, e.g., you could have monadic computations of typem Int,m Char,m [Int], etc.Inside the implementation of
MyStateT, we will exploit the polymorphism ofmto choose a convenient type for the result which helps to implement the new monadLet us see now a possible implementation of
MyStateTnewtype MyStateT s m a = MyStateT { runMyStateT :: s -> m (a,s) }The state monad transformer models computations as a function which takes a state (of type
s) and returns a computationm(the underlying monad).Importantly, the computation
mproduces a result (of typea) and the resulting state (of types).Observe how the state monad transformer uses
mto keep track of the resulting state by simply instantiating the typem's result to be(a, s).
Given the implementation of
MyStateT, we now show thatMyStateT s m ais a monad for every underlying monadm.When defining
returnand(>>=)forMyStateT, we are going to usereturnand(>>=)given form— recall that we should seemas black-box!return x = MyStateT \ s -> return (x, s)
Observe that
returnforMyStateTusesreturn (x,s) :: m (a, s).For the bind, we start by indicating that the result of the bind is a monadic value from our monad transformer. Therefore, we know that
MyStateT f >>= k = MyStateT \ s1 -> ...
We are going to use the types to guide us in completing the definitions.
We know that
k :: a -> MyStateT s m bis waiting for a value of typea, and we have the statefull computationf :: s -> m (a,s)which produces anawhen given a state of types. We have states1 :: sin scope, let's use it!MyStateT f >>= k = MyStateT \ s1 -> do (a, s2) <- f s1 ... (k a) ...Observe that
(k a) :: MyStateT s m b, but the type for the whole missing code must be... (k a) ... :: m (b, s). As a first step, we can destructMyStateTand obtain its underlying computation of types -> m (b, s).MyStateT f >>= k = MyStateT \ s1 -> do (a, s2) <- f s1 runMyStateT (k a) ...Still, the type for
runMyStateT (k a) :: s -> m (b, s), i.e., we need to provide a state of typesto obtain a term of typem (b, s). At this point, we have two states in scope:s1ands2. Which one should we use? SinceMyStateT s m ais a state monad, it should "pass along" the state between subcomputations. Thus, we will pass the resulting state after runningf s1, i.e.,s2.MyStateT f >>= k = MyStateT \ s1 -> do (a, s2) <- f s1 runMyStateT (k a) s2Now that
MyStateT s mis a monad (do not forget to prove the monadic laws), we also need to provide the non-proper morphismsgetandputcorresponding to state monads:instance Monad m => MonadState s (MyStateT s m) where get = MyStateT \ s -> return (s, s) put s = MyStateT \ _ -> return ((),s)We leave it as an exercise to define the run functions
evalMyStateT :: Monad m => MyStateT s m a -> s -> m a runMyStateT :: Monad m => MyStateT s m a -> s -> m (a, s)
Monad transformers: lifting non-proper morphisms
So far, we have shown how
MyStateT s m atakes a monadmand synthesizes a state monad withmunderneath.For that, we show how
returnand(>>=)forMyStateTcan be defined based onreturnand(>>=)for monadm.What about the non-proper morphisms of
m? Can we reuse them inMyStateT s m?A monad transformer is also responsible to provide the tools to take a non-proper morphism in
mand lift it to work inMyStateT s m.We declare a type-class for monad transformers, which contains a method for lifting operations from the underlying monad:
class Monad m => MT m mT | mT -> m where lift :: m a -> mT aThis type class denotes that
mTis a monad transformer which takesmas its underlined monad.The type for
lifttakes a monadic operation inm(of typem a) and returns a computation that works inmTinstead (of typemT a).We show that
MyStateTis a monad transformer:instance Monad m => MT m (MyStateT s m) where lift m = MyStateT \ s -> do a <- m return (a, s)Observe that
liftonly runs the computationmwithout changing the state.
MyExceptT: an error monad transformer
As before, we start by defining a data type to host the transformed monad, i.e., the monad which the transformer considers.
data MyExceptT e m a
To implement
MyExceptT e m a, we need to add a mechanism to track if a computation has successfully or erroneously produced a value.We will exploit the polymorphism of
mto instantiate the type of the result with a data type which can keep track of errors.data MyExceptT e m a = MyExceptT { runMyExceptT :: m (Either e a) }
We define
MyExceptT e mas a monadAs before, to define
returnand(>>=)forMyExceptT e m, we are to usereturnand(>>=)for the underling monadmThe
returnfunction forMyExceptT e mreturns a successful computation, i.e., a value of the formRight xfor somex.return a = MyExceptT $ return $ Right a
Observe the use of
returnfrom the underlying monadm, wherereturn (Right a) :: m (Either e a).To define
m >>= kforMyExceptT e m, the bind must check that if the computationmfails, thenm >>= kmust also fail — thus, propagating the error. Otherwise, the bind must give the result ofmtokand run the subsequent computation. With this in mind, we start defining the bind forMyExceptT e m.MyExceptT m >>= k = MyExceptT ...
The bind needs to check if the computation
mproduces a value or an error. To observe the value thatmproduces, we have no other choice than to run it and extract the resulting value — recall that we considermas a black-box. For that, we use the(>>=)from the underlying monad.MyExceptT m >>= k = MyExceptT $ m >>= \ result -> ...We capture what
mproduces in the variableresult. Then, we inspect it to see if it is a legit value or an error.MyExceptT m >>= k = MyExceptT $ m >>= \ result -> case resullt of ...If
resultis an error, i.e., a value of the formLeft e, we propagate it.MyExceptT m >>= k = MyExceptT $ m >>= \case Left e -> return (Left e) ...Observe that
return (Left e) :: m (Either e a). In case thatresultis of the formRight a, i.e., a legit value, the bind needs to continue running the computation as indicating byk— for that, we need to give it the value produced bym.MyExceptT m >>= k = MyExceptT $ m >>= \case Left e -> return (Left e) Right a -> ... k a ...Observe, however, that
k a :: MyExceptT e m bwhile we need an expression of typem (Either e b), which is contained ink a. Thus, we extract it by simply applyingrunMyExceptT.MyExceptT m >>= k = MyExceptT $ m >>= \case Left e -> return (Left e) Right a -> runMyExceptT (k a)Now that
MyExceptT e mis a monad (again, do not forget to prove the monadic laws), we need to provide the non-proper morphismsthrowErrorandcatchErrorfor error monads.throwError e = MyExceptT $ return (Left e)
Observe that
return (Left e) :: m (Either e a). To definecatchError, we need to observe if the computation passed as first argument produces a legit value or an error.catchError (MyExceptT m) h = MyExceptT $ ...
Computation
mis given in the underlying monad. Therefore, to observe the value produced bym, we need to run it and extract its result with the(>>=)from the underlying monad.catchError (MyExceptT m) h = MyExceptT $ m >>= \ result -> ...We then need to inspect the shape of result. In case that
resultis of the formRight a(for some valuea),catchErrordoes not need to engage the exception handler, but rather return this value.catchError (MyExceptT m) h = MyExceptT $ m >>= \case Right a -> return (Right a) ...If
minstead produces an error,catchErrorneeds to engage the exception handler.catchError (MyExceptT m) h = MyExceptT $ m >>= \case Right a -> return (Right a) Left e -> ... (h e) ...We have the type for
(h e) :: MyExceptT e m a, but we need an expression of typem (Either e a)for the code... (h e) .... For that, we deconstructh eby usingrunMyExceptT.catchError (MyExceptT m) h = MyExceptT $ m >>= \case Right a -> return (Right a) Left e -> runMyExceptT (h e)We leave it as an exercise to define the run function
runMyExceptT :: MyExceptT e m a -> m (Either e a)
We show that
MyExceptT eis a monad transformer by providing a lifting operationinstance Monad m => MT m (MyExceptT e m) where lift m = MyExceptT $ m >>= \ a -> return (Right a)The
liftfunction simply runs the computationmfrom the underlying monad and wraps its result with aRightconstructor.
MyReaderT: a reader monad transformer
We start by defining a data type to host the transformed monad, i.e., the monad which the transformer considers.
type MyReaderT r m a
To implement
MyReaderT r m a, we need to add a mechanism to provide a computationm awith some environment (read-only state).newtype MyReaderT r m a = MyReaderT { runMyReaderT :: r -> m a}For that, we make
m adepend on an environment of typer.**Exercise**: Define `MyReaderT r m` as a monad, i.e, implement `return` and `(>>=)`.**Exercise**: Show that `MyReaderT r m` is a reader monad, i.e., implement functions `ask` and `local`.**Exercise**: Show that `MyReaderT r m` is a monad transformer, i.e., implement the `lift` function.
Interpreter 6: an interpreter with MyStateT, MyExceptT, and MyReaderT
We define the monad stack using our own monad transformers
type Eval a = (MyStateT Store (MyReaderT Env (MyExceptT Err Identity)) a )We can see our monad stack graphically as follows
We adapt the run function accordingly
runEval :: Eval a -> Either Err a runEval m = m & (`evalMyStateT` emptyStore) & (`runMyReaderT` emptyEnv) & runMyExceptT & runIdentity
To run a computation of type Eval a, we need to run the whole monad stack.
Lifting operations I
When introducing the new definition for
Evalin the code for Interpreter 4, there are some auxiliary functions which do not type check.We start by focusing on the auxiliary function
lookupVar:lookupVar :: Name -> Eval Value lookupVar x = do env <- ask case Map.lookup x env of Nothing -> throwError (UnboundVariable x) Just v -> return vThis code now does not type check for two reasons.
Firstly, the
askfunction has typeMyReaderT Env (MyExceptT Err Identity) a, while thelookupVaris defined for the whole stack, i.e, a monad of typeMyStateT Store (MyReaderT Env (MyExceptT Err Identity)).Graphically, we have an operation in one for the layers of the monad stack and we want to make it work for the whole stack.
We then lift
askto work with the top-level layer (MyStateT).lookupVar x = do env <- lift ask -- new case Map.lookup x env of Nothing -> throwError (UnboundVariable x) Just v -> return vSecondly,
throwErrorhas typeMyExceptT Err Identity a, whilelookupVaris defined for whole monad stack.As before, we have an operation in one of the layers of the monad stack and we want to make it work for the whole stack. In this case, however, we need to lift it two layers up.
In the code, function
liftis applied twice.lookupVar :: Name -> Eval Value lookupVar x = do env <- lift ask case Map.lookup x env of Nothing -> lift (lift (throwError (UnboundVariable x))) -- new Just v -> return vThe definition for
lookupVarabove type-checks.Similar to
lookupVar, auxiliary functionlocalScopedoes not type check and needs to be changed. However, the reason why it does not work cannot be simply fixed by calling functionlift(explained below).
Lifting operations II
It is not always as easy as applying
liftin order to use a non-proper morphism from the underlying layersTo illustrate this point, we will examine the code for the constructor
Catch.In the interpreter 4, the code was
eval (Catch e1 e2) = catchError (eval e1) (\ _err -> eval e2)
However, this line of code does not type check if we use our own monad transformers.
The reason for that is that
eval e1 :: Eval a \ _err -> eval e2 :: Err -> Eval a
and function
catchErroris working at the layer introduced byMyExceptT. Because of that,catchErrorexpects two arguments, let's called itarg1andarg2, with the following typesarg1 :: MyExceptT Err Identity a arg2 :: e -> MyExceptT Err Identity a
To feed
catchErrorwith the appropriate monadic values, we need to run the computations of typeEval ato remove all the upper layers until reaching the right one, i.e., the error layer.
eval (Catch e1 e2) =
let st1 = runSTInt (eval e1)
st2 = runSTInt (eval e2)
...
So far, we obtained functions st1 and st2 which produce, when given an
state, a monadic computation in the next layer of the stack (i.e., a reader
monadic computation). In other words, to run st1 and st2, and therefore
going deeper into the monad stack, it is necessary to provide a state. Since
we have none in scope, we need to break the abstraction of the MyStateT
monad transformer in order to introduce a binding for the state.
eval (Catch e1 e2) = MyStateT \ s -> do
let
st1 = runSTInt (eval e1)
st2 = runSTInt (eval e2)
env1 = runEnv (st1 s)
env2 = runEnv (st2 s)
...
At this point, we are at the level of the reader monad in our stack.
Function env1 and env2 produce, when given an environment, a computation
in the error layer monad. As before, since we do not have an environment in
scope, we need to break the abstraction of the MyReaderT monad transformer
to introduce a binding for the environment.
eval (Catch e1 e2) = MyStateT \ s -> do
let
st1 = runSTInt (eval e1)
st2 = runSTInt (eval e2)
env1 = runEnv (st1 s)
env2 = runEnv (st2 s)
MyReaderT \ r -> ...
With the environment r in scope, env1 r :: MyExceptT Err Identity a and
env2 r :: MyExceptT Err Identity a and we can feed function catchError with them.
eval (Catch e1 e2) = MyStateT \ s -> do
let
st1 = runSTInt (eval e1)
st2 = runSTInt (eval e2)
env1 = runEnv (st1 s)
env2 = runEnv (st2 s)
MyReaderT \ r -> catchError (env1 r) (\ _err -> env2 r)
We have not described how to adapt the function
localScopeand we leave it as an exercise since it exhibits a similar problem aseval (Catch e1 e2).In Interpreter 4, GHC's "deriving magic" saves you from all the complications of lifting non-proper morphisms.
If you design your own monad transformer, however, it is highly probably that GHC does not know how to derive it.
Summary
The combination of effects does not always compose.
The order in which monad transformers are applied matters
It is not semantically the same to first apply the state and then the error monad transformer, than the other way around
We show implementations for state, error, and reader monad transformers.
Lifting non-proper morphism is not always trivial.
- Sometimes, we need to break the abstraction of upper layers until we get to
the layer where the non-proper morphism resides, apply it, and then lift the
result back to the top layer by either applying the monad transformers'
constructors or the right amount of
liftfunctions.
- Sometimes, we need to break the abstraction of upper layers until we get to
the layer where the non-proper morphism resides, apply it, and then lift the
result back to the top layer by either applying the monad transformers'
constructors or the right amount of