Skip to content

MaybeT

Our implementation of display in the previous subsection simply aborts if the command line arguments aren't what we expected or the file to be display cannot be read. This is exactly what we normally implement using the Maybe monad. We can't quite use the Maybe monad because our implementation of display needs to IO monad to read command line arguments, read the file to be displayed, and either print the file or pass it to less. In the previous subsection, we simply used solved this problem by explictly checking return values. In this section, we will use the MaybeT monad transformer to equip the IO monad with the abortion logic of the Maybe monad:

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

Instead of simply a value of type Maybe a, MaybeT is a wrapper around an action in the monad m that returns a value of type Maybe a. Let's figure out how we turn this into a monad. The logic of our Maybe monad was that a sequence of steps aborts as soon as the first step in such a computation aborts. It succeeds only if all of the steps in the computation succeed:

instance Monad Maybe where
    return  = Just
    x >>= f = maybe Nothing f x

We also figured out how to bolt the behaviour of the Reader monad on top of any existing monad m. The key was to thread the context not through a sequence of applications of pure functions but through a sequence of actions in the underlying monad m. We do the same to equip an existing monad with the abortion logic of Maybe:

instance Monad m => Monad (MaybeT m) where
    return x = MaybeT $ return $ Just x
    x >>= f  = MaybeT $ runMaybeT x >>= maybe (return Nothing) (runMaybeT . f)

return x cannot fail, but the result of the computation wrapped by MaybeT should be of type m (Maybe a). Thus, we wrap x in Just and then return it using the return method of our monad m.

x >>= f runs x and binds its return value to maybe (return Nothing) (runMaybeT . f). If x returns Just y, then we apply runMaybeT . f to y, that is, we first apply f to y to obtain a value of type MaybeT m b, and then we run this computation using runMaybeT, which gives us a value of the desired type m (Maybe b). If x returns Nothing, the next step in the computation also returns Nothing. The result of the whole computation is Nothing, only wrapped in our monad m as required. More importantly, f is never run. This is really important here. If the logic of x >>= f is that the computation should abort as soon as x fails, then we don't want to see any of the effects that f may have; we really need to abort the computation.