Monad transformers

Real code often needs several effects at once: state + errors + IO. But monads, unlike functors, do not compose automatically: m(na)m\,(n\,a) is not a monad in general. A transformer is a version of a monad with a “slot” for another monad.

Transformers are defined as the same monads, but with a monad parameter inside:

newtype MaybeT  m a   = MaybeT  (m (Maybe a))
newtype ExceptT e m a = ExceptT (m (Either e a))
newtype StateT  s m a = StateT  (s -> m (a, s))
The nested contexts IO (Maybe Int) are equivalent to the transformer stack MaybeT IO Int
Fig. 2.7. Nested boxes are the transformer stack: IO  (Maybe  Int)\mathtt{IO\;(Maybe\;Int)} MaybeT  IO  Int\cong \mathtt{MaybeT\;IO\;Int}; lift\mathtt{lift} raises a base-monad action up the stack.

There is a monad called Identity, defined like this:

newtype Identity a = Identity { runIdentity :: a }

instance Monad Identity where
  return           = Identity     -- wrap a value
  Identity x >>= f = f x          -- and immediately unwrap it

Ordinary monads are the same transformers over the identity monad:

type State s = StateT s Identity
MaybeT Identity aMaybe a       -- Maybe is not a type synonym, but it is isomorphic

Actions of the base monad are raised through the layers of the stack by lift\mathtt{lift}; its laws require consistency with pure\mathtt{pure} and >>=\mathbin{\mathtt{>>=}}:

class MonadTrans t where
  lift :: Monad m => m a -> t m a

lift . pure == pure
lift (m >>= f) == lift m >>= (lift . f)

The order of the stack is the semantics: the same set of effects, a different order — different behavior on error:

StateT s (Except e) as -> Either e (a, s)    -- an error eats the state
ExceptT e (State s) as -> (Either e a, s)     -- the state survives an error

Let's put it all together in a live example: a chain of IO actions, each of which may yield no result (the environment variable is missing, the file is missing). Without a transformer you would have to check Nothing\mathtt{Nothing} after every step; MaybeT  IO\mathtt{MaybeT\;IO} hides those checks inside do notation — the first Nothing\mathtt{Nothing} aborts the whole chain.

import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
import Control.Monad.Trans.Class (lift)
import System.Environment        (lookupEnv)      -- :: String -> IO (Maybe String)
import System.Directory          (doesFileExist)

readConfig :: FilePath -> IO (Maybe String)       -- Nothing if the file is missing
readConfig path = do
  exists <- doesFileExist path
  if exists then Just <$> readFile path else pure Nothing

-- IO + possible failure in a single do block:
loadConfig :: MaybeT IO String
loadConfig = do
  lift (putStrLn "reading config...")   -- lift an ordinary IO action
  path <- MaybeT (lookupEnv "CONFIG")   -- no variable -> the whole chain is Nothing
  MaybeT (readConfig path)              -- no file     -> the whole chain is Nothing

main :: IO ()
main = do
  result <- runMaybeT loadConfig        -- runMaybeT :: MaybeT IO a -> IO (Maybe a)
  case result of
    Just cfg -> putStrLn ("ok: " ++ cfg)
    Nothing  -> putStrLn "config not found"