Monad transformer
monadicon.icon
Overview
For some monads, we can define monad transformers.
A monad transformer takes an existing monad to a new monad.
The order in which we apply monad transformers matters.
Not all monads have associated transformers.
There is no transformer version of IO
Monad transformers provide a way to build monads out of smaller building blocks and to combine effects as needed.
Unfortunately, the underlying theory is not extremely beautiful, leading to several restrictions and complications.
A few approaches have been tried to address some of the shortcomings, but mtl remains dominant.
monad transformers allow us to nest monadic computations in a stack with an interface to exchange values between the levels, called lift.
liftIO :: IO a -> m a
liftIO lifts all the way up to the top regardless of the depth of the nest. This can be used for IO action only
Known transformers
IdentityT is the identity transformer, which maps a monad to (something isomorphic to) itself. This may seem useless at first glance, but it is useful for the same reason that the id function is useful -- it can be passed as an argument to things which are parameterized over an arbitrary monad transformer, when you do not actually want any extra capabilities.
StateT adds a read-write state.
ReaderT adds a read-only environment.
WriterT adds a write-only log.
RWST conveniently combines ReaderT, WriterT, and StateT into one.
MaybeT adds the possibility of failure.
ErrorT adds the possibility of failure with an arbitrary type to represent errors.
ContT adds continuation handling.
code:transformers.hs
newtype ReaderT r m a = ReaderT {runReaderT :: r -> m a}
newtype StateT s m a = StateT {runStateT :: s -> m (a,s)}
newtype MaybeT m a = MaybeT {runMaybeT :: m (Maybe a)}
newtype ExceptT e m a = ExceptT {runExceptT :: m (Either e a)}
Visualization of monad transformers
https://gyazo.com/b0e90bea99a57cd590f692051f44927f
Newtype deriving
Using monad transformer involves a lot of lifting.
A newtype makes it easier to still treat Eval as abstract for most of the code.
The GeneralizedNewtypeDeriving extension makes it possible to lift class instances from the inner type to the wrapper type in the obvious way.
code:GND.hs
newtype Eval a = Eval (ExceptT String Identity a)
deriving ( Functor
, Applicative
, Monad
, MonadError String
)
Example interpreter
code:interpreter.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-} module Interpreter where
import Control.Monad.Identity
import Control.Monad.Except
import Control.Monad.State
import Data.Map
import qualified Data.Map as M
import Data.Monoid ((<>))
type Env = Map String Int
data Expr =
Lit Int
| Add Expr Expr
| Div Expr Expr
| Var String
| Assign String Expr
| Seq Expr Expr
newtype Eval a = Eval (StateT Env (ExceptT String Identity) a)
deriving (Functor, Applicative, Monad, MonadState Env, MonadError String)
eval :: Expr -> Eval Int
eval (Lit n) = pure n
eval (Add e1 e2) = (+) <$> eval e1 <*> eval e2
eval (Div e1 e2) = doDiv e1 e2
eval (Var x) = varLookup x
eval (Seq e1 e2) = eval e1 >> eval e2
eval (Assign x e) = varSet x e
doDiv :: Expr -> Expr -> Eval Int
doDiv e1 e2 = do
v1 <- eval e1
v2 <- eval e2
if v2 == 0
then divByZeroError
else return (v1 div v2)
divByZeroError :: Eval ()
divByZeroError = throwError "Division by 0"
varLookup :: String -> Eval Int
varLookup x = do
env <- get
case M.lookup x env of
Nothing -> unknownVar x
Just num -> return num
varSet :: String -> Expr -> Eval Int
varSet x e = do
v <- eval e
modify (M.insert x v)
return v
unknownVar :: String -> Eval ()
unknownVar x = throwError $ "Variable not found: " <> show x
runEval :: Eval a -> Env -> Either String a
runEval (Eval m) env = runIdentity (runExceptT (evalStateT m env))
program :: Expr
program = Assign "x" (Lit 10)
Seq Assign "x" (Div (Var "x") (Lit 2))
Seq Add (Var "x") (Lit 1)
Useful links
Useful links (Japanese)