Monads Can Be Used for Many Things!
- Partial Functions
- Global Variables
- Parsing
- Exceptions
- Test Generation
- Concurrency
- …
Exception Handling
Recall our expressions with division
data Expr
= Number Int -- ^ 0,1,2,3,4
| Plus Expr Expr -- ^ e1 + e2
| Div Expr Expr -- ^ e1 / e2
deriving (Show)
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
We defined a Result
type
data Result a = Ok a | Err String
made it a Monad
instance Monad Result where
return x = Ok x
Ok v) >>= f = f v
(Err s) >>= _ = Err s (
and then we can write
eval :: Expr -> Result Int
Number n) = return n
eval (Plus e1 e2) = do {n1 <- eval e1; n2 <- eval e2; return (n1 + n2) }
eval (Div e1 e2) = do { n1 <- eval e1;
eval (<- eval e2;
n2 if n2 /= 0
then return (n1 `div` n2)
else Err ("DBZ: " ++ show e2)
}
which doesn’t crash but returns an Err
>>> eval (Div (Number 10) (Plus (Number 5) (Number (-5))))
Err "DBZ: Plus (Number 5) (Number (-5))"
and when it succeeds it returns an Ok
>>> eval (Div (Number 10) (Plus (Number 5) (Number (-5))))
Ok 1
Generalizing Result
to Either
The standard library generalizes the Result
type to Either
data Result a = Err String | Ok a
data Either e a = Left e | Right a
Err s
becomesLeft s
Ok v
becomesRight v
Result a
becomesEither String a
(But we can data other than String
in the Left
values)
EXERCISE: Generalizing Result
Monad to Either
Monad
Lets translate the old Monad
instance for Result
instance Monad Result where
-- return :: a -> Result a
return x = Ok x
-- (>>=) :: Result a -> (a -> Result b) -> Result b
Ok v) >>= f = f v
(Err s) >>= _ = s (
into a Monad
instance for Either
instance Monad (Either e) where
-- return :: a -> Either e a
return x = ???
-- (>>=) :: Either e a -> (a -> Either e b) -> Either e b
Right v) >>= f = ???
(Left s) >>= _ = ??? (
QUIZ
We can rewrite eval
to return an Either
eval :: Expr -> Either Expr Int
Number n) = return n
eval (Plus e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 return (n1+n2)
Div e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 if n2 /= 0
then return (n1 `div` n2)
else Left e2
What does quiz
evaluate to?
= eval (Div (Val 10) (Plus (Number 5) (Number (-5)))) quiz
A. Err "DBZ: Plus (Number 5) (Number (-5))"
B. Left "DBZ: Plus (Number 5) (Number (-5))"
C. Run-time Exception
D. Plus (Number 5) (Number (-5))
E. Left (Plus (Number 5) (Number (-5)))
Either
is an Exception Monad!
What can you do with exceptions?
throwError
an exception (with some value) …catchError
an exception (and use its value) …
1. throw
ing an Exception
We can simply define
throw :: e -> Either e a
= Left exn throw exn
and now voila
eval :: Expr -> Either Expr Int
Number n) = return n
eval (Plus e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 return (n1 + n2)
Div e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 if n2 /= 0
then return (n1 `div` n2)
else throw e2
Exactly the same evaluator
Result is a
Left
==> an exception came all the way to the top.Either
monad ensures the “exception” shoots to the top!
>>> eval (Div (Numer 10) (Plus (Number 5) (Number (-5))))
Left (Minus (Number 5) (Number 5))
No further evaluation happens after a throw
because ???
catch
ing an exception
How to catch an exception?
Lets change our Expr
type to
data Expr
= Number Int -- ^ 0,1,2,3,4
| Plus Expr Expr -- ^ e1 + e2
| Div Expr Expr -- ^ e1 / e2
| Try Expr Int -- ^ try e1 n
deriving (Show)
Informally, try e n
evaluates to e
but
if
e
is undefined due to divide-by-zerothen evaluate to
n
eval :: Expr -> Either Expr Int
Number n) = return n
eval (Plus e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 return (n1+n2)
Div e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 if n2 /= 0
then return (n1 `div` n2)
else throw e2
Try e n) = catch (eval e) (\_ -> return n) eval (
QUIZ
What should the type of catch
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
catch :: Either e a -> (e -> Either e a) -> Either e a
catch (Left e) handle = ???
catch (Right a) handler = ???
eval :: Expr -> Either Expr Int
Number n) = return n
eval (Plus e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 return (n1+n2)
Div e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2 if n2 /= 0
then return (n1 `div` n2)
else throw e2
Try e n) = catch (eval e) (\_ -> return n)
eval (
= Div (Number 10) (Plus (Number 5) (Number (-5)))
e1 = Try e1 7
e1'
= eval (Try e1 7) quiz
What does quiz
evaluate to?
A. Right 7
B. Left 7
C. Right 0
D. Left 0
E. Left (Plus (Number 5) (Number (-5)))
Either
is an Exception Monad!
throw
an exception (with some value) …catch
an exception (and use its value) …
throw :: e -> Either e a
= Left e
throw e
catch :: Either e a -> (e -> Either e a) -> Either e a
catch (Left e) handle = handle e
catch (Right e) _ = Right e
Monads Can Be Used for Many Things!
- Partial Functions
- Global State
- Parsing
- Exceptions
- Test Generation
- Concurrency
- …
… but what if I want Exceptions and Global State ?
Mixing Monads
What if I want Exceptions and Global State ?
Profiling with the ST Monad
Lets implement a profiling monad that counts the number of operations
-- A State-Transformer with a "global" `Int` counter
type Profile a = State Int a
We can write a runProfile
that
- executes the transformer from
0
- and renders the result
runProfile :: (Show a) => Profile a -> String
= showValCount (runState st 0)
runProfile st
showValCount :: (Show v, Show c) => (v, c) -> String
= "value: " ++ show val ++ ", count: " ++ show count showValCount (val, count)
A function to increment the counter
count :: Profile ()
= do
count <- get
n +1) put (n
A Profiling Evaluator
We can use count
to write a profiling evaluator
evalProf :: Expr -> Profile Int
= eval
evalProf where
Number n) = return n
eval (Plus e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2
countreturn (n1+n2)
Div e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2
countreturn (n1 `div` n2)
And now, as there are two operations, we get
>>> e1
Div (Number 10) (Plus (Number 5) (Number 5))
>>> runProfile (evalProf e1)
"value: 1, count: 2"
But what about Divide-by-Zero?
Bad things happen…
>>> e2
Div (Number 10) (Plus (Number 5) (Number (-5)))
>>> runProfile (evalProf e2)
*** Exception: divide by zero
"value:
Problem: How to get global state AND exception handling ?
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.
Mixing Monads with Transformers
Step 1: Specifying Monads with Extra Features
Step 2: Implementing Monads with Extra Features
Specifying Monads with Extra Features
First, instead of using concrete monads
- e.g.
Profile
orEither
We will use type-classes to abstractly specify a monad’s capabilities
- e.g.
MonadState s m
orMonadError e m
A Class for State-Transformers Monads
The class MonadState s m
defined in the Control.Monad.State
says
m
is a State-Transformer 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
!
Generalize Types to use Classes
So we can generalize the type of count
to use MonadState Int m
count :: (MonadState Int m) => m ()
= do
count <- get
n +1) put (n
A Class for Exception Handling Monads
The class MonadError e m
defined in [Control.Monad.Except
][6] 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
!
Generalize Types to use Classes
So we can generalize the type of tryCatch
to use MonadError e m
tryCatch :: (MonadError e m) => m a -> a -> m a
= catchError m (\_ -> return def) tryCatch m def
Generalize eval
to use Constraints
We can now specify that eval
uses a monad m
that implements
MonadState Int
andMonadError Expr
eval :: (MonadState Int m, MonadError Expr m) => Expr -> m Int
Number n) = return n
eval (Plus e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2
countreturn (n1 + n2)
Div e1 e2) = do n1 <- eval e1
eval (<- eval e2
n2
countif (n2 /= 0)
then return (n1 `div` n2)
else throwError e2
Try e n) = tryCatch (eval e) n eval (
Lets try to run it!
>>> e1
>>> evalMix e1
... GHC yells "please IMPLEMENT this MAGIC monad that implements BOTH features"
Mixing Monads with Transformers
Step 1: Specifying Monads with Extra Features
Step 2: Implementing Monads with Extra Features
Implementing Monads with Extra Features
Transform2 (Transform1 m)
implements
m
operations and- operations added by
Transform1
and - operations added by
Transform2
We require
- A basic monad
m
- A Transform1 that adds
State
capabilities - A Transform2 that adds
Exception
capabilities
A Basic Monad
First, lets make a basic monad
- only implements
>>=
andreturn
data Identity a = Id a
instance Monad Identity where
return a = Id a
Id a) >>= f = f a (
A very basic monad: just a wrapper (Id
) around the value (a
)
- No extra features
A Transform that adds State
Capabilities
The transformer StateT s m
defined in the Control.Monad.State
module
- takes as input monad m
and
- transforms it into a new monad
m'
such that m'
implements
all the operations that
m
implementsand adds State-transformer capabilities
StateT s m
satisfies the constraint (MonadState s (StateT s m))
A State-transformer over Int
states
type Prof = StateT Int Identity
We can go back and give evalProf
the type
evalProf :: Expr -> Prof Int
A Transform that adds Except
ion Capabilities
The transformer ExceptT e m
- takes as input a monad
m
and - transforms it into a new monad
m'
such that m'
implements
all the operations that
m
implementsand adds Exception-handling capabilities
ExceptT e m
satisfies the constraint (MonadError e (ExceptT e m))
An Exception Handler Monad with Expr
-typed exceptions
type Exn = ExceptT Expr Identity
We can go back and give evalThrowCatch
the type
evalThrowCatch :: Expr -> Exn Int
Composing Transformers
We can use both transformers to get both powers!
type ExnProf a = ExceptT Expr (StateT Int (Identity)) a
ExnProf
implements State-transformer-over Int
and Exception-handling-over-Expr
EXERCISE: Executing the Combined Transformer
Recall that
type ExnProf a = ExceptT Expr (StateT Int (Identity)) a
Lets write a function
runExnProf :: (Show a) => ExnProf a -> String
= ??? runExnProf epm
such that
>>> runExnProf (eval e1)
"value: 1, count: 2"
>>> runExnProf (eval e2)
"Plus (Number 5) (Number (-5)) after 2 operations"
TRY AT HOME: Combining in a Different Order
We can also combine the transformers in a different order
type ProfExn a = StateT Int (ExceptT Expr (Identity)) a
ExnProf
implements State-transformer-over Int
and Exception-handling-over-Expr
Can you implement the function
runProfExn :: (Show a) => ProfExn a -> String
such that when you are done, we can get the following behavior?
>>> runProfExn (eval e1)
"value: 1, count: 2"
>>> runProfExn (eval e2)
"Left (Plus (Number 5) (Number (-5)))"
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!