My guess is that a MonadTrans
transforms a Monad
into something else, instead of transforming a Monad
into a Monad
. It's more generalized, since you might write something that transforms a Monad
and you can define lift
, but you can't define >>=
and return
. Since most (if not all) MonadTrans
instances end up being Monad
s, it doesn't really present a problem as the compiler still handles it just fine.
Why isn't lift's return value constrained to be a monad?
-
26-06-2022 - |
Frage
Why isn't MonadTrans
defined as
class MonadTrans t where
lift :: (Monad m, Monad (t m)) => m a -> t m a
-- ^^^^^^^^^^^
instead of current
class MonadTrans t where lift :: Monad m => m a -> t m a
This is Haskell 98 (unlike the suggestion in Why aren't monad transformers constrained to yield monads?) and ensures that the result will always be a monad. Is there a reason why monad transformers are allowed to produce something that isn't a monad?
Lösung 2
Andere Tipps
bheklilr's answer gave me idea of an example where a monad transformer produces something that isn't a monad. A well-known example of something that isn't a monad is ZipList
. And we can make a variant that runs a monadic action at each level:
import Control.Applicative
import Control.Arrow ((***))
import Control.Monad
import Control.Monad.Trans
-- | A list where each step is produced by a monadic action.
data ListT m a = Nil | Cons (m (a, ListT m a))
This is actually a monad stream. And it can be easily made into a Functor
and an Applicative
instance Monad m => Functor (ListT m) where
fmap f Nil = Nil
fmap f (Cons k) = Cons $ (f *** fmap f) `liftM` k
instance Monad m => Applicative (ListT m) where
pure x = Cons $ return (x, pure x)
Cons mf <*> Cons mx = Cons $ do
(f, fs) <- mf
(x, xs) <- mx
return (f x, fs <*> xs)
_ <*> _ = Nil
but obviously not a monad. So we have a MonadTrans
instance that converts a monad into something that's only an Applicative
.
instance MonadTrans ListT where
lift mx = Cons $ (\x -> (x, lift mx)) `liftM` mx
(This whole thing made me realize that an experimental ZipSink
in conduit-extra is also a nice such example.)
However, this raises another question: If we want such transformers, what laws should they adhere to? The laws for MonadTrans
are defined as
lift . return = return lift (m >>= f) = lift m >>= (lift . f)
So in our case we could wish for something like
lift (f `liftM` x) = fmap f (lift x)
lift . return = pure
lift (m `ap` f) = lift m <*> lift f
I'm going to disagree with the other two answers to say that the result should be a monad. The reason why is that otherwise there are no sensible laws that lift
should obey.
lift
is supposed to be a monad morphisms, meaning that it should obey the following two laws:
lift (return x) = return x
lift (m >>= f) = lift m >>= \r -> lift (f r)
These laws make more sense when you realize they are functor laws between two Kleisli categories:
-- i.e. "fmap id = id"
(lift .) return = return
-- i.e. "fmap (f . g) = fmap f . fmap g"
(lift .) (f >=> g) = (lift .) f >=> (lift .) g
However, if you don't constrain the output to be a monad then those laws are no longer valid and you have no sensible way to verify that you implemented lift
correctly.
I suspect the real reason was to make the class Haskell98