Simplest non-trivial monad transformer example for "dummies", IO+Maybe

Could someone give a super simple (few lines) monad transformer example, which is non-trivial (i.e. not using the Identity monad - that I understand).

For example, how would someone create a monad that does IO and can handle failure (Maybe)?

What would be the simplest example that would demonstrate this?

I have skimmed through a few monad transformer tutorials and they all seem to use State Monad or Parsers or something complicated (for a newbee). I would like to see something simpler than that. I think IO+Maybe would be simple, but I don't really know how to do that myself.

How could I use an IO+Maybe monad stack? What would be on top? What would be on bottom? Why?

In what kind of use case would one want to use the IO+Maybe monad or the Maybe+IO monad? Would that make sense to create such a composite monad at all? If yes, when, and why?


Solution 1:

This is available here as a .lhs file.

The MaybeT transformer will allow us to break out of a monad computation much like throwing an exception.

I'll first quickly go over some preliminaries. Skip down to Adding Maybe powers to IO for a worked example.

First some imports:

 import Control.Monad
 import Control.Monad.Trans
 import Control.Monad.Trans.Maybe

Rules of thumb:

In a monad stack IO is always on the bottom.

Other IO-like monads will also, as a rule, always appear on the bottom, e.g. the state transformer monad ST.

MaybeT m is a new monad type which adds the power of the Maybe monad to the monad m - e.g. MaybeT IO.

We'll get into what that power is later. For now, get used to thinking of MaybeT IO as the maybe+IO monad stack.

Just like IO Int is a monad expression returning an Int, MaybeT IO Int is a MaybeT IO expression returning an Int.

Getting used to reading compound type signatures is half the battle to understanding monad transformers.

Every expression in a do block must be from the same monad.

I.e. this works because each statement is in the IO-monad:

 greet :: IO ()                               -- type:
 greet = do putStr "What is your name? "      -- IO ()
            n <- getLine                      -- IO String
            putStrLn $ "Hello, " ++ n         -- IO ()

This will not work because putStr is not in the MaybeT IO monad:

mgreet :: MaybeT IO ()
mgreet = do putStr "What is your name? "    -- IO monad - need MaybeT IO here
            ...

Fortunately there is a way to fix this.

To transform an IO expression into a MaybeT IO expression use liftIO.

liftIO is polymorphic, but in our case it has the type:

liftIO :: IO a -> MaybeT IO a

 mgreet :: MaybeT IO ()                             -- types:
 mgreet = do liftIO $ putStr "What is your name? "  -- MaybeT IO ()
             n <- liftIO getLine                    -- MaybeT IO String
             liftIO $ putStrLn $ "Hello, " ++ n     -- MaybeT IO ()

Now all of the statement in mgreet are from the MaybeT IO monad.

Every monad transformer has a "run" function.

The run function "runs" the top-most layer of a monad stack returning a value from the inside layer.

For MaybeT IO, the run function is:

runMaybeT :: MaybeT IO a -> IO (Maybe a)

Example:

ghci> :t runMaybeT mgreet 
mgreet :: IO (Maybe ())

ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()

Also try running:

runMaybeT (forever mgreet)

You'll need to use Ctrl-C to break out of the loop.

So far mgreet doesn't do anything more than what we could do in IO. Now we'll work on an example which demonstrates the power of mixing the Maybe monad with IO.

Adding Maybe powers to IO

We'll start with a program which asks some questions:

 askfor :: String -> IO String
 askfor prompt = do
   putStr $ "What is your " ++ prompt ++ "? "
   getLine

 survey :: IO (String,String)
 survey = do n <- askfor "name"
             c <- askfor "favorite color"
             return (n,c)

Now suppose we want to give the user the ability to end the survey early by typing END in response to a question. We might handle it this way:

 askfor1 :: String -> IO (Maybe String)
 askfor1 prompt = do
   putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
   r <- getLine
   if r == "END"
     then return Nothing
     else return (Just r)

 survey1 :: IO (Maybe (String, String))
 survey1 = do
   ma <- askfor1 "name"
   case ma of
     Nothing -> return Nothing
     Just n  -> do mc <- askfor1 "favorite color"
                   case mc of
                     Nothing -> return Nothing
                     Just c  -> return (Just (n,c))

The problem is that survey1 has the familiar staircasing issue which doesn't scale if we add more questions.

We can use the MaybeT monad transformer to help us here.

 askfor2 :: String -> MaybeT IO String
 askfor2 prompt = do
   liftIO $ putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
   r <- liftIO getLine
   if r == "END"
     then MaybeT (return Nothing)    -- has type: MaybeT IO String
     else MaybeT (return (Just r))   -- has type: MaybeT IO String

Note how all of the statemens in askfor2 have the same monad type.

We've used a new function:

MaybeT :: IO (Maybe a) -> MaybeT IO a

Here is how the types work out:

                  Nothing     :: Maybe String
           return Nothing     :: IO (Maybe String)
   MaybeT (return Nothing)    :: MaybeT IO String

                 Just "foo"   :: Maybe String
         return (Just "foo")  :: IO (Maybe String)
 MaybeT (return (Just "foo")) :: MaybeT IO String

Here return is from the IO-monad.

Now we can write our survey function like this:

 survey2 :: IO (Maybe (String,String))
 survey2 =
   runMaybeT $ do a <- askfor2 "name"
                  b <- askfor2 "favorite color"
                  return (a,b)

Try running survey2 and ending the questions early by typing END as a response to either question.

Short-cuts

I know I'll get comments from people if I don't mention the following short-cuts.

The expression:

MaybeT (return (Just r))    -- return is from the IO monad

may also be written simply as:

return r                    -- return is from the MaybeT IO monad

Also, another way of writing MaybeT (return Nothing) is:

mzero

Furthermore, two consecutive liftIO statements may always combined into a single liftIO, e.g.:

do liftIO $ statement1
   liftIO $ statement2 

is the same as:

liftIO $ do statement1
            statement2

With these changes our askfor2 function may be written:

askfor2 prompt = do
  r <- liftIO $ do
         putStr $ "What is your " ++ prompt ++ " (type END to quit)?"
         getLine
  if r == "END"
    then mzero      -- break out of the monad
    else return r   -- continue, returning r

In a sense, mzero becomes a way of breaking out of the monad - like throwing an exception.

Another example

Consider this simple password asking loop:

loop1 = do putStr "Password:"
           p <- getLine
           if p == "SECRET"
             then return ()
             else loop1

This is a (tail) recursive function and works just fine.

In a conventional language we might write this as a infinite while loop with a break statement:

def loop():
    while True:
        p = raw_prompt("Password: ")
        if p == "SECRET":
            break

With MaybeT we can write the loop in the same manner as the Python code:

loop2 :: IO (Maybe ())
loop2 = runMaybeT $
          forever $
            do liftIO $ putStr "Password: "
               p <- liftIO $ getLine
               if p == "SECRET"
                 then mzero           -- break out of the loop
                 else return ()

The last return () continues execution, and since we are in a forever loop, control passes back to the top of the do block. Note that the only value that loop2 can return is Nothing which corresponds to breaking out of the loop.

Depending on the situation you might find it easier to write loop2 rather than the recursive loop1.

Solution 2:

Suppose you have to work with IO values that "may fail" in some sense, like foo :: IO (Maybe a), func1 :: a -> IO (Maybe b) and func2 :: b -> IO (Maybe c).

Manually checking for the presence of errors in a chain of binds quickly produces the dreaded "staircase of doom":

do
    ma <- foo
    case ma of
        Nothing -> return Nothing
        Just a -> do
            mb <- func1 a
            case mb of
                Nothing -> return Nothing
                Just b -> func2 b

How to "automate" this in some way? Perhaps we could devise a newtype around IO (Maybe a) with a bind function that automatically checks if the first argument is a Nothing inside IO, saving us the trouble of checking it ourselves. Something like

newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }

With the bind function:

betterBind :: MaybeOverIO a -> (a -> MaybeOverIO b) -> MaybeOverIO b
betterBind mia mf = MaybeOverIO $ do
       ma <- runMaybeOverIO mia
       case ma of
           Nothing -> return Nothing
           Just a  -> runMaybeOverIO (mf a)

This works! And, looking at it more closely, we realize that we aren't using any particular functions exclusive to the IO monad. Generalizing the newtype a little, we could make this work for any underlying monad!

newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }

And this is, in essence, how the MaybeT transformer works. I have left out a few details, like how to implement return for the transformer, and how to "lift" IO values into MaybeOverM IO values.

Notice that MaybeOverIO has kind * -> * while MaybeOverM has kind (* -> *) -> * -> * (because its first "type argument" is a monad type constructor, that itself requires a "type argument").