haskelloption-typemonad-transformers

Nothing generated by running a monad transformers stack is not == Nothing?


Repros:

cabal repl --build-depends=mtl-prelude,transformers
λ> import Data.Mayb
λ> import Control.Monad
λ> import Control.Monad.Trans.Identity
λ> import MTLPrelude
λ> :{
ghci| maybeQuit :: MonadPlus m => Maybe Char -> MaybeT m (Maybe Char)
ghci| maybeQuit key = do
ghci|   case key of
ghci|     Just 'q' -> mzero
ghci|     Just '\ESC' -> mzero
ghci|     _ -> return key
ghci| :}
λ> runIdentityT $ runMaybeT $ maybeQuit (Just 'c')
Just (Just 'c')
λ> runIdentityT $ runMaybeT $ maybeQuit (Just 'q')
Nothing

So far so good.

But then:

λ> (runIdentityT $ runMaybeT $ maybeQuit (Just 'q')) == Nothing 
False
λ> isNothing (runIdentityT $ runMaybeT $ maybeQuit (Just 'q'))
False

What?!


I do see that expression is a bit polymorphic,

λ> :t runIdentityT $ runMaybeT $ maybeQuit (Just 'q')
runIdentityT $ runMaybeT $ maybeQuit (Just 'q')
  :: MonadPlus f => f (Maybe (Maybe Char))

but I'm not sure at all how this can imply that something printed as Nothing could ever be other than (==) Nothing.


For context, I'm running maybeQuit in a monadic stack (there's MaybeT and other transformers in it) with a IO at the bottom, and it is working as expected, but it's implementations, hence the type, does not require the power of IO, so I was trying to test it in a monad other than IO, and here's how I came to ask this question.

In hindsight I do start seeing something else I don't quite understand: I see :t runMaybeT $ maybeQuit (Just 'q') is MonadPlus m => m (Maybe (Maybe Char)), which is what I expect, but then applying runIdentityT to that gives type MonadPlus f => f (Maybe (Maybe Char), which is the same thing, so I must be misunderstanding the meaning/purpose of IdentityT, and can't help but thinking this is the whole reason I don't get what I think I should get in the original example.

In light of the above reasoning, I've tried to force m === [] in the expression runMaybeT $ maybeQuit (Just 'c') by applying head to it, and the result is more what I expect:

λ> isNothing (head $ runMaybeT $ maybeQuit (Just 'q'))
True
λ> isNothing (head $ runMaybeT $ maybeQuit (Just 'c'))
False

but I still don't understand what's wrong with using runIdentityT.


Solution

  • To understand what's going on, consider this much simpler example:

    ghci> import Data.Maybe
    ghci> weird = return Nothing
    ghci> weird
    Nothing
    ghci> weird == Nothing
    False
    ghci> isNothing weird
    False
    ghci> :t weird
    weird :: Monad m => m (Maybe a)
    ghci> 
    

    When you just enter weird, GHCi defaults m to be IO (so the overall type is IO (Maybe a)), and then evaluates the IO action for you, which results in Nothing. When you enter weird == Nothing or isNothing weird, m is forced to be Maybe (so the overall type is Maybe (Maybe a)), so return Nothing becomes Just Nothing, which is different than Nothing.

    Your example has a couple of extra layers of indirection, but at the end of the day, the same thing is happening. runIdentityT was a red herring; your expression is so polymorphic that it doesn't do anything at all where it is.

    If you're confused as to why your expression is basically the same as return Nothing, remember that the mzero you're using is defined like this:

    instance (Monad m) => MonadPlus (MaybeT m) where
        mzero = MaybeT (return Nothing)
    

    Not this:

    instance (MonadPlus m) => MonadPlus (MaybeT m) where
        mzero = MaybeT mzero