Real code often needs several effects at once: state + errors + IO. But monads, unlike functors, do not compose automatically: 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))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 itOrdinary monads are the same transformers over the identity monad:
type State s = StateT s Identity
MaybeT Identity a ≅ Maybe a -- Maybe is not a type synonym, but it is isomorphicActions of the base monad are raised through the layers of the stack by ; its laws require consistency with and :
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) a ≅ s -> Either e (a, s) -- an error eats the state
ExceptT e (State s) a ≅ s -> (Either e a, s) -- the state survives an errorLet'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 after every step; hides those checks inside do notation — the first 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"