haskellfunctional-programmingoption-typemonadsmonad-transformers

Why doesn't MonadMaybe exist?


The way I look at MonadState, for instance, is that any type (or set of types, e.g. ReaderT r m a) that implements it, must support get+put (or alternatively just state) in order to behave like the State monad, which is indeed a Monad, but not only a Monad, as get+put is the API characterizes it.

But then MonadMaybe would be a class that a monadic stack should implement if it wanted to behave like... a Maybe? What does that even mean? I mean, Maybe is Monad, but what else? It doesn't offer any API that characterizes it, does it?

I've read Edward Kmett's answer on reddit, but I'm not sure I understand. Probably I'm drowing in a inch of water.

What would that typeclass look like?

class Monad m => MonadMaybe m where
...???

Concretely, say I have this

foo :: Monad m => MaybeT (StateT Int (ReaderT Int m)) Int
foo = do
  s <- get
  r <- ask
  if s + r == 0
    then mzero -- see note (¹) at the bottom
    else return $ s + r

This is a monadic stack which hardcodes the order of the effects provided by State and Reader, so it can be run like this

bar :: IO (Maybe Int)
bar = runMaybeT foo `evalStateT` 3 `runReaderT` 3

but not like this

bar :: IO (Maybe Int)
bar = runMaybeT (foo `evalStateT` 3 `runReaderT` 3)

because the monadic layers have to be peeled off from the outside, i.e. in the order MaybeT->StateT->ReaderT.

One way to free bar from that obligation is to require that m implements the appropriate typeclasses, like this

foo :: (MonadState Int m, MonadReader Int m) => MaybeT m Int
foo = do
  s <- get
  r <- ask
  if s + r == 0
    then mzero
    else return $ s + r

Now both bar implementations above work.

At this point, I can't help but thinking that if I had MonadMaybe, I could rewrite the signature of foo like this:

foo :: (MonadMaybe m, MonadState Int m, MonadReader Int m) => m Int

That would mean that bar could be implemented in any of these ways,

bar :: IO (Maybe Int)
bar = (runMaybeT foo) `evalStateT` 3 `runReaderT` 3 -- MSR
bar = (runMaybeT foo) `runReaderT` 3 `evalStateT` 3 -- MRS
bar = runMaybeT (foo `runReaderT` 3) `evalStateT` 3 -- RMS
bar = runMaybeT (foo `evalStateT` 3) `runReaderT` 3 -- SMR
bar = runMaybeT (foo `runReaderT` 3 `evalStateT` 3) -- RSM
bar = runMaybeT (foo `evalStateT` 3 `runReaderT` 3) -- SRM

where I've used permutations of MSR to mean the order in which the 3 layers are processed.


(¹) Changing runMaybeT to (runMaybeT . forever) allows to succintly express a computation that can run forever or exit early via mzero.


Solution

  • What Kmett's answer boils down to is that you can rewrite your final foo signature as:

    foo :: (MonadPlus m, MonadState Int m, MonadReader Int m) => m Int
    

    and all your bars run fine. MonadMaybe isn't needed because the interface in MonadPlus is already sufficient.

    Full example:

    import Control.Monad
    import Control.Monad.State
    import Control.Monad.Reader
    import Control.Monad.Trans.Maybe
    
    foo :: (MonadPlus m, MonadState Int m, MonadReader Int m) => m Int
    foo = do
      s <- get
      r <- ask
      if s + r == 0
        then mzero
        else return $ s + r
    
    main :: IO ()
    main = do
      print =<< runMaybeT foo `evalStateT` 3 `runReaderT` 3 -- MSR
      print =<< runMaybeT foo `runReaderT` 3 `evalStateT` 3 -- MRS
      print =<< runMaybeT (foo `runReaderT` 3) `evalStateT` 3 -- RMS
      print =<< runMaybeT (foo `evalStateT` 3) `runReaderT` 3 -- SMR
      print =<< runMaybeT (foo `runReaderT` 3 `evalStateT` 3) -- RSM
      print =<< runMaybeT (foo `evalStateT` 3 `runReaderT` 3) -- SRM