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 = lift mzero