Outline
- Generalizing Exception Handling
- Generalizing Mutable State
- Combining Exception Handling and Mutable State
Exception Handling
Recall our expressions with division
data Expr
= Num Int
| Plus Expr Expr
| Div Expr Expr
We had a potentially crashing evaluator
eval :: Expr -> Int
Number n) = n
eval (Plus e1 e2) = eval e1 + eval e2
eval (Div e1 e2) = eval e1 `div` eval e2
eval (
-- >>> eval (Div (Val 10) (Plus (Number 5) (Number (-5))))
-- Exception: Divide by zero
Exception Handling with Either
We used the standard library type Either
, which is an instance of Monad
type Result a = Either String a
eval :: Expr -> Result Int
Num v) = return v
eval (Plus e1 e2) = do
eval (<- eval e1
v1 <- eval e2
v2 return (v1 + v2)
Div e1 e2) = do
eval (<- eval e1
v1 <- eval e2
v2 if v2 == 0
then Left ("DBZ " ++ show e2)
else return (v1 `div` v2)
This doesn’t crash but returns a Left
>>> eval (Div (Number 10) (Plus (Number 5) (Number (-5))))
Left "DBZ: Plus (Number 5) (Number (-5))"
And when it succeeds it returns a Right
>>> eval (Div (Number 10) (Plus (Number 5) (Number 5)))
Right 1
Exception Handling Monads
This implementation is specific to Either
:
Div e1 e2) =
eval (do
...
if v2 == 0
then Left ("DBZ " ++ show e2) -- using Left here!
else return (v1 `div` v2)
We would like to generalize this to any monad that provides two methods:
throwError
(with some value)catchError
(and use its value)
Let’s first implement these functions for Either
1. Throwing an Exception
We can simply define
throwError :: e -> Either e a
= Left exn throwError exn
and rewrite the Div
case:
Div e1 e2) =
eval (do
...
if v2 == 0
then throwError ("DBZ " ++ show e2) -- using a general function
else return (v1 `div` v2)
2. Catching an Exception
What does it mean to catch an exception?
Lets change our Expr
type to
data Expr
= Num Int
| Plus Expr Expr
| Div Expr Expr
| Try Expr -- Try to evaluate an expression,
-- but if it fails, return 0
We would like to implement eval
for Try
as follows:
Try e) = catchError (eval e) (\_ -> return 0)
eval (-- ^ this is the computation that might fail
-- ^ this is the handler
-- (might depend on the exception!)
QUIZ
With this implementation of eval
:
eval :: Expr -> Either String Int
Try e) = catchError (eval e) (\_ -> return 0) eval (
What should the type of catchError
be?
A. Either e a -> (a -> Either e b) -> Either e b
B. Either e a -> (e -> Either e b) -> Either e b
C. Either e a -> (e -> Either e a) -> Either e a
D. Either e a -> Either e a -> Either e a
E. Either e a -> Either e b -> Either e b
Implementing catch
Lets implement the catch
function!
catch :: Either e a -> (e -> Either e a) -> Either e a
catch (Left e) handler = ???
catch (Right a) handler = ???
QUIZ
Num v) = return v
eval (Plus e1 e2) = ...
eval (Div e1 e2) = do ...
eval (if v2 == 0
then Left ("DBZ " ++ show e2)
else return (v1 `div` v2)
Try e) = catchError (eval e) (\_ -> return 0)
eval (
Left e) handler = handler e
catchError (Right a) _ = Right a
catchError (
= Div (Number 10) (Plus (Number 5) (Number (-5)))
e1
= eval (Try e1) quiz
What does quiz
evaluate to?
A. Right 0
B. Left 0
C. Left "DBZ: (Plus (Number 5) (Number (-5)))"
A Class for Exception Handling Monads
The class MonadError e m
defined in Control.Monad.Except
says
m
is a Exception-Handling monad with exception typee
class Monad m => MonadError e m where
throwError :: e -> m a
catchError :: m a -> (e -> m a) -> m a
That is to say, m
implements
>>=
andreturn
operations specified byMonad
andthrowError
andcatchError
operations specified byMonadError
!
Generalizing the DBZ Evaluator
Now we can generalize our DBZ evaluator to work with any MonadError
!
-- | Attention: new type!
eval :: (MonadError String m) => Expr -> m Int
Num v) = ...
eval (Plus e1 e2) = ...
eval (Div e1 e2) = do
eval (<- eval e1
v1 <- eval e2
v2 if v2 == 0
then throwError ("DBZ " ++ show e2)
else return (v1 `div` v2)
Try e) = catchError (eval e) (\_ -> return 0) eval (
Running the Generalized Evaluator
Lets try to run it!
>>> eval e0
error: Ambiguous type variable ‘m0’...
What is the problem?
eval
is now polymorphic inm
(does not know whichm
to use)- similar to the
read "2"
problem we saw earlier - we need to tell GHC which instance of
MonadError
to use - we know just the guy for the job:
Either String
!
For example, we can add an annotation:
>>> eval e0 :: Either String Int
Right 3
Outline
- Generalizing Exception Handling [done]
- Generalizing Mutable State
- Combining Exception Handling and Mutable State
Mutable State
Recall our expression with a counter:
data Expr
= Num Int
| Plus Expr Expr
| Next
-- 0
Next
==> 0
-- 0 1
Plus Next Next
==> 1
-- 0 1 2
Plus Next (Plus Next Next)
==> 3
Counting with State
We used the standard library type State
, which is an instance of Monad
type Cnt = Int
type Counting a = State Cnt a
eval :: Expr -> Counting Int
Num n) = return n
eval (Plus e1 e2) = do v1 <- eval e1
eval (<- eval e2
v2 return (v1 + v2)
Next = do
eval <- get
cnt <- put (cnt + 1)
_ return (cnt)
A Class for Mutable State Monads
Just like exception handing, threading mutable state is a common pattern!
So let’s define a class for it!
The class MonadState s m
defined in the Control.Monad.State
says
m
is a State-Threading monad with state types
class Monad m => MonadState s m where
get :: m s
put :: s -> m ()
That is to say, m
implements
>>=
andreturn
operations specified byMonad
andget
andput
operations specified byMonadState
!
Generalizing the Counting Evaluator
So we can generalize our counting evaluator to work with any MonadState
!
-- | Attention: new type!
eval :: (MonadState Cnt m) => Expr -> m Int
Num n) = return n
eval (Plus e1 e2) = do v1 <- eval e1
eval (<- eval e2
v2 return (v1 + v2)
Next = do
eval <- get
cnt <- put (cnt + 1)
_ return (cnt)
And define a topEval
function that tells GHC to use State
:
- because
evalState :: State s a -> s -> a
topEval :: Expr -> Int
= evalState (eval e) 0
topEval e
>>> topEval (Plus Next Next)
1
>>> topEval (Plus Next (Plus Next Next))
3
What was the point of generalizing the evaluators?
- We could now use a different
MonadError
(notEither
) - We could now use a different
MonadState
(notState
) - But the coolest thing is that we can now combine them!
Outline
- Generalizing Exception Handling [done]
- Generalizing Mutable State [done]
- Combining Exception Handling and Mutable State
A Fancy Evaluator
What if I want Exceptions and Mutable State?
data Expr
= Num Int
| Plus Expr Expr
| Div Expr Expr -- Needs exceptions
| Next -- Needs state
-- omitting Try for simplicity
It would be great if we could just mesh the two evaluators together!
Num n) = return n
eval (Plus e1 e2) = do v1 <- eval e1
eval (<- eval e2
v2 return (v1 + v2)
-- This case is from the DBZ evaluator:
-- it uses `throwError`!
Div e1 e2) = do
eval (<- eval e1
v1 <- eval e2
v2 if v2 == 0
then throwError ("DBZ " ++ show e2)
else return (v1 `div` v2)
-- This case is from the Counting evaluator:
-- it uses `get` and `put`!
Next = do
eval <- get
cnt <- put (cnt + 1)
_ return (cnt)
But what type should eval
have?
eval :: Expr -> ??? Int
QUIZ
What should be the type of eval
for the Fancy evaluator?
-- (A)
eval :: Expr -> State Cnt (Either String Int)
-- (B)
eval :: Expr -> Either String (State Cnt Int)
-- (C)
eval :: (MonadState Cnt m) => Expr -> m Int
-- (D)
eval :: (MonadError String m) => Expr -> m Int
-- (E)
eval :: (MonadState Cnt m, MonadError String m) => Expr -> m Int
Composing Constraints
This is the best part about generalizing: constraints compose!
-- The type has both constraints we need:
eval :: (MonadState Cnt m, MonadError String m) => Expr -> m Int
Num n) = ...
eval (Plus e1 e2) = ...
eval (-- We can use `throwError` because `m` is a `MonadError`!
Div e1 e2) = do
eval (...
if v2 == 0
then throwError ("DBZ " ++ show e2)
else return (v1 `div` v2)
-- We can use `get` and `put` because `m` is *also* a `MonadState`!
Next = do
eval <- get
cnt <- put (cnt + 1)
_ return cnt
Running the Fancy Evaluator
All is well until we try to run it:
>>> eval (Div (Num 3) Next)
error: ambiguous type variable ‘m0’...
We are in a pickle:
- We need a monad
m
that is both aMonadState
and aMonadError
State
is not our guy (why?)Either
isn’t either (why?)- Do we need to implement a new monad from scratch? :(
Monad Transformers to the rescue!
Mixing Monads with Transformers
Start with a Basic Monad
m
implements
- no special operations
Transform it to add some Capabilities
Transform1 m
implements
m
operations and- operations added by
Transform1
Transform again to add more Capabilities
Transform2 (Transform1 m)
implements
m
operations and- operations added by
Transform1
and - operations added by
Transform2
… And so on
Transform3 (Transform2 (Transform1 m))
implements
m
operations and- operations added by
Transform1
and - operations added by
Transform2
and - operations added by
Transform3
…
Reminiscent of the Decorator Design Pattern or Python’s Decorators.
A Basic Monad
First, lets make a basic monad
- only implements
>>=
andreturn
- adds no effects to the computation
- just a wrapper around a value of type
a
data Identity a = Id a
EXERCISE: Monad Instance for Identity
Implement the Monad
instance for Identity
:
data Identity a = Id a
instance Monad Identity where
return a = ???
>>= f = ??? x
A Basic Monad
First, lets make a basic monad
- only implements
>>=
andreturn
- adds no effects to the computation
- just a wrapper around a value of type
a
data Identity a = Id a
instance Monad Identity where
return a = Id a
Id a) >>= f = f a (
Adding Exception Capabilities
The transformer ExceptT e m
defined in Control.Monad.Except
- takes as input a monad
m
and - transforms it into a new monad
m'
such that m'
implements
- all the operations that
m
implements - and adds exception-handling capabilities
- that is, it can
throwError
andcatchError
In other words:
ExceptT e m
satisfies the constraint(MonadError e (ExceptT e m))
DBZ Evaluator using transformers
For example, we could give our (non-generalized) DBZ evaluator the type:
-- instead of: type Result a = Either String a
type Result a = ExceptT String Identity a
eval :: Expr -> Result Int
...
Running Transformers
If we run this evaluator, we get:
>>> eval e0
ExceptT (Id (Right 3))
The result is wrapped in all these layers of monads…
Each monad/transformer M
provides a function runM
that “unwraps” the results:
runIdentity :: Identity a -> a
runExceptT :: ExceptT e m a -> m (Either e a)
So we can do:
>>> runIdentity (runExceptT (eval e0))
Right 3
Adding State Capabilities
The transformer StateT s m
defined in the Control.Monad.State
- takes as input monad
m
and - transforms it into a new monad
m'
such that m'
implements
- all the operations that
m
implements - and adds state-threading capabilities
- e.g. it can
get
andput
the state
In other words:
StateT s m
satisfies the constraint(MonadState s (StateT s m))
Counting Evaluator Using Transformers
For example, we could give our (non-generalized) DBZ evaluator the type:
-- instead of: type Counting a = State Int a
type Counting a = StateT Int Identity a
eval :: Expr -> Counting Int
...
And we can run it using runStateT
(or evalStateT
):
runStateT :: StateT s m a -> s -> m (a, s)
evalStateT :: StateT s m a -> s -> m a
>>> runIdentity (runStateT (eval (Plus Next Next)) 0)
1,2)
(
>>> runIdentity (evalStateT (eval (Plus Next Next)) 0)
1
Fancy Evaluator Using Transformers
We can stack both transformers on top of each other!
type Fancy a = ExceptT String (StateT Cnt Identity) a
EXERCISE: Running Fancy
type Fancy a = ExceptT String (StateT Cnt Identity) a
Lets write a function
runFancy :: Fancy a -> Either String a
= ??? runFancy x
such that
>>> runFancy (eval (Div (Num 3) (Plus Next Next)))
Right 3
>>> >>> runFancy (eval (Div (Num 3) Next))
Left "DBZ: Next"
Summary: Mixing Monads with Many Features
1. Transformers add capabilities to Monads
Transform2 (Transform1 m)
implements
m
operations and- operations added by
Transform1
and - operations added by
Transform2
2. StateT
and ExceptT
add State and Exceptions
- Start with a basic monad
Identity
- Use
StateT Int
to add global-Int
state-update capabilities - Use
ExceptT Expr
to add exception-handling capabilities
Play around with this in your homework assignment!