Note: This is a paraphrased rip-off of the excellent article A Gentle Introduction to Monad Transformers. Almost none of these ideas are my own. I simply paired down the original post to something I thought was manageable for our study group.
- What they are
- Why you'd want them
- How they work
A monad transformer is a specialized version of a monad that allows it to be combined with other monads.
- To add configuration (
Reader) capability to an IO-heavy program instead of relying on ENV - To add "short-circuiting" capabilities (
Either) to my HTTP request-handling code - To add logging capability (
Writer) to my test mocks (i.e. "assert getFooById was called with arg1 and arg2")
Let's take a look at the Monad type class:
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m bNote: When binding (>>=) the m in the second parameter provided (a -> m b) cannot change. Since do-notation is just syntactic sugar around bind, this means that you're stuck using the same m throughout a do-block.
For instance, m is chosen in the following block to be IO, so we can't use the Maybe instance of Monad:
main = do
contents <- readFile "foo.txt"
case listToMaybe contents of
Just _ -> putStrLn "File was not empty."
Nothing -> ()This would result in a type error:
main = do
contents <- readFile "foo.txt"
listToMaybe contents -- can't intermix
putStrLn "File was not empty."Restricting ourselves to one monad per function can be a bummer.
data GameError = InvalidInput String
| IncorrectGuess deriving (Show, Eq)
promptForGuess :: IO (Either GameError String)
promptForGuess = do
putStrLn "Enter a guess:"
guess <- getLine
return (validateInput guess)validateInput :: String -> Either GameError String
validateInput guess = if (foldr (\item acc -> acc && isAlpha item) True guess)
then Right guess
else Left (InvalidInput guess)checkGuess :: String -> String -> IO (Either GameError String)
checkGuess fileName guess = do
contents <- readFile fileName
return $ maybe (Left IncorrectGuess) (return) $ find (==guess) (lines contents)printResults :: Either GameError String -> IO ()
printResults (Left (InvalidInput guess)) = putStrLn $ "\"" ++ guess ++ "\" is not valid input."
printResults (Left (IncorrectGuess)) = putStrLn $ "Nope. That last guess was wrong."
printResults (Right guesses) = putStrLn ("Your two guesses (" ++ guesses ++ ") were correct.")game :: IO (Either GameError String)
game = do
putStrLn "I have lived in many cities. Can you guess one?"
e1 <- promptForGuess
case e1 of
err@(Left _) -> return err
Right g1 -> do
r1 <- checkGuess "cities.txt" g1
case e1 of
err@(Left _) -> return err
Right m1 -> do
putStrLn "I like a lot of bands. Can you guess one?"
e2 <- promptForGuess
case e2 of
err@(Left _) -> return err
Right g2 -> do
r2 <- checkGuess "bands.txt" g2
case r2 of
Right m2 -> return $ Right $ m1 ++ " and " ++ m2
err -> return errmain :: IO ()
main = game >>= printResultsNote that in the game function we are explicitly pattern-matching on the constructor of Either GameError String in order to short-circuit our computation. The Either Monad instance does this for us:
instance Monad (Either e) where
return = Right
Right m >>= k = k m
Left e >>= _ = Left e...but we can't intermix our choice of m in the function k passed to bind (>>=).
Monad transformers allow us to add the functionality of one monad into a different monad, producing a new super-awesome monad with the powers of both. We'll begin by adding Either functionality to the IO monad.
Let's create a new type EitherIO that represents IO combined with Either:
data EitherIO e a = EitherIO { runEitherIO :: IO (Either e a) }We can create values of type Either IO e a from a value of IO (Either e a):
*Main> let f = undefined :: IO (Either e a)
*Main> let g = EitherIO f
*Main> :t g
g :: EitherIO e a
..and go the other way, too:
*Main> :t runEitherIO
runEitherIO :: EitherIO e a -> IO (Either e a)
*Main> :t runEitherIO g
runEitherIO g :: IO (Either e a)
I'll refer to converting from IO (Either e a) as "wrapping," and converting from EitherIO e a as "unwrapping."
Our new type EitherIO e a needs to instantiate Monad, and thus needs to instantiate Applicative and Functor type classes. I'm just gonna copypaste from elsewhere and try to explain.
Given a value of type EitherIO e a and function f of type a -> b, we produce a new value EitherIO e a that when run:
- runs the underlying
IOcomputation, producing eitherLeft eorRight a - if
Left e, leave it alone (results inIO (Left e)) - if
Right a, applyftoa(results inIO (Right (f a)))
instance Functor (EitherIO e) where
fmap f = EitherIO . fmap (fmap f) . runEitherIOI can't explain this one.
instance Applicative (EitherIO e) where
pure a = EitherIO $ return (Right a)
EitherT f <*> EitherIO v = EitherIO $ f >>= \mf -> case mf of
Left e -> return (Left e)
Right k -> v >>= \mv -> case mv of
Left e -> return (Left e)
Right x -> return (Right (k x))When >>= is applied to x and f a new EitherIO e a is produced that when run:
- Runs the underlying IO action
- If that action produced a
Left e, returnIO (Left e)(resulting inIO (Left e)) - If that action produced a
Right a, applyftoa(resulting inIO (Right (f a)))
-- runEitherIO :: EitherIO e a -> IO (Either e a)
instance Monad (EitherIO e) where
return a = EitherIO $ return (Right a)
m >>= k = EitherIO $ do
a <- runEitherIO m
case a of
Left l -> return (Left l)
Right r -> runEitherT (k r)What this enables is for us to build sequences of short-circuiting (like Either) computations that interact with the outside world (like IO).
bad :: EitherIO String Int
bad = EitherIO $ do
putStrLn "bad!"
return $ Left "bummer"
good :: EitherIO String Int
good = EitherIO $ do
putStrLn "good"
return $ Right 100
ex3 = do
v1 <- good
v2 <- bad -- short-circuit here
v3 <- good
return (v1, v2)*Main> runEitherIO ex3
good
bad!
Left "bummer"
So, that works great if all of our functions are of the type IO (Either e a). But what happens if we have some functions (like those in the Prelude) that are of the type IO a? How can we use them in our new EitherIO monad?
To accomplish this, we'll implement lift, which allows us "get at" functions in an inner monad from our outer, more powerful monad.
lift :: IO a -> EitherIO e a
lift x = EitherIO (fmap Right x)This function is given a value of type IO a and produces an EitherIO e a that, when run:
- Runs the original
IO acomputation (producing a) - Wraps the results in a
Rightand returns (producesIO (Right a)) - Wraps that result in
EitherIO
Let's use lift alongside our other functions
ex4 = do
v1 <- good
v2 <- good
lift $ putStrLn "Fun in the sun."
return (v1, v2)I implemented EitherIO for three reasons:
- This guy did it too
- Our program used
IO (Either e a)already - Programming in the concrete helps me understand things before generalizing
As it turns out, though, we've implemented something very close to EitherT, which allows you to wrap any monad with Either-like short-circuiting functionality. The important thing to note is that EitherT does not know anything about the monad that it wraps. In fact, that monad could itself be a wrapped monad!
data EitherT e m a = EitherT { runEitherT :: m (Either e a) }
{-
data EitherIO e a = EitherIO { runEitherIO :: IO (Either e a) }
-}instance Functor m => Functor (EitherT e m) where
fmap f = EitherT . fmap (fmap f) . runEitherTinstance Applicative m => Applicative (EitherT e m) where
pure = EitherT . pure . Right
f <*> x = EitherT $ liftA2 (<*>) (runEitherT f) (runEitherT x)
{-
instance Applicative (EitherIO e) where
pure = EitherIO . pure . Right
f <*> x = EitherIO $ liftA2 (<*>) (runEitherIO f) (runEitherIO x)
-}instance Monad m => Monad (EitherT e m) where
return = EitherT . return . Right
x >>= f = EitherT $ runEitherT x >>= either (return . Left) (runEitherT . f)
{-
instance Monad (EitherIO e) where
return = pure
x >>= f = EitherIO $ runEitherIO x >>= either (return . Left) (runEitherIO . f)
-}lift :: Functor m => m a -> EitherT m e a
lift x = EitherT (fmap Right m)
{-
lift :: Functor m => m a -> EitherT e m a
lift x = EitherT (fmap Right x)
-}Using EitherT to wrap IO with Either-like functionality, our right-leaning game function cleans up nicely:
game :: IO (Either GameError String)
game = runEitherT $ do
lift $ putStrLn "I have lived in many cities. Can you guess one?"
g1 <- EitherT promptForGuess
m1 <- EitherT $ checkGuess "cities.txt" g1
lift $ putStrLn "I like a lot of bands. Can you guess one?"
g2 <- EitherT promptForGuess
m2 <- EitherT $ checkGuess "bands.txt" g2
return (m1 ++ " and " ++ m2)
{-
game :: IO (Either GameError String)
game = do
putStrLn "I have lived in many cities. Can you guess one?"
e1 <- promptForGuess
case e1 of
err@(Left _) -> return err
Right g1 -> do
r1 <- checkGuess "cities.txt" g1
case e1 of
err@(Left _) -> return err
Right m1 -> do
putStrLn "I like a lot of bands. Can you guess one?"
e2 <- promptForGuess
case e2 of
err@(Left _) -> return err
Right g2 -> do
r2 <- checkGuess "bands.txt" g2
case r2 of
Right m2 -> return $ Right $ m1 ++ " and " ++ m2
err -> return err
}