So in transformers
I see,
class (Monad m) => MonadIO m where
-- | Lift a computation from the 'IO' monad.
liftIO :: IO a -> m a
instance MonadIO IO where
liftIO = id
and I understand that the reason that this differs from MonadTrans is that if you have some M1T (M2T (M3T (M4T IO))) x
made of 4 composed monad transformers, then you don't want to lift . lift . lift . lift $ putStrLn "abc"
but you'd rather just liftIO $ putStrLn "abc"
.
But, this specificity for IO
seems very weird when the fundamental definition above seems to be this weird set of recursions with liftIO
. It seems like there should either be a newtype declaration for some combinator like (ExceptT :~: MaybeT) IO x
so that a single lift
is all you ever need (I suppose this is a monad transformer transformer?), or else some multi-param type class,
class (Monad m) => MonadEmbed e m
-- | Lift a computation from the `e` monad.
embed :: e a -> m a
instance (Monad m) => MonadEmbed m m where
embed = id
Why doesn't transformers
use one of these approaches so that the MonadTrans sequences do not have to be rooted in IO
? Is it just the fact that the transformers handle all "other" effects so that the only things at the very bottom are either Identity
(already handled with return :: a -> m a
) or IO
? Or does the above require something like UndecidableInstances which the transformers
library is loathe to include? Or what?
Monad transformers allow developers to compose the effects of different monads, even if the monads themselves are not the same. An example is writing a do-statement that can: abort computation (ExceptT), thread state (StateT), and connect to a database (via a Haskell library such as persistence or esqueleto).
A Monad that can convert any given IO[A] into a F[A] , useful for defining parametric signatures and composing monad transformer stacks.
But, this specificity for
IO
seems very weird
I challenge the assumption that this is specific to IO
. I also see many other classes in mtl
. For example:
class Monad m => MonadError e m | m -> e where
throwError :: e -> m a
catchError :: m a -> (e -> m a) -> m a
class Monad m => MonadState s m | m -> s where
get :: m s
put :: s -> m ()
state :: (s -> (a, s)) -> m a
...and many others. Generally, the "mtl
way" to build monadic actions is to use these typeclass-polymorphic operations, so that one need never lift
-- rather, just monomorph the operation to the appropriate lifted type. For example, MonadError
completely replaces a hypothetical liftMaybe :: MonadMaybe m => Maybe a -> m a
: rather than lifting a Maybe a
value, one would have the producer of the Maybe a
value call throwError
and return
instead of Nothing
and Just
.
It seems like there should be a newtype declaration for some combinator like
(ExceptT :~: MaybeT) IO x
so that a single lift is all you ever need
With this proposal, you would need (at least) two different kinds of lifts: one lift to go from m a
to trans m a
, and one lift to go from trans m a
to (trans' :~: trans) m a
. Having a single operation that handles both kinds of lifting is more uniform.
It seems like there should be some multi-param type class,
class Monad m => MonadEmbed e m -- | Lift a computation from the `e` monad. embed :: e a -> m a instance Monad m => MonadEmbed m m where embed = id
This approach looks deceptively nice at first. However, if you try to write and use this class, you will quickly discover why all the mtl
classes include functional dependencies: the instance MonadEmbed m m
is surprisingly hard to choose! Even a very simple example like
embed (Right ()) :: Either String ()
is an ambiguity error. (After all, we only know that Right 3 :: Either a ()
for some a
-- we don't yet know that a ~ String
and so we can't choose the MonadEmbed m m
instance!) I suspect most of your other instances will run into similar problems. If you add the obvious functional dependencies, your type inference problems go away but the fundep checks greatly restrict you: one may only lift from the base monad and not from arbitrary intermediate monads as you might hope. This is such a painful problem in practice (and the "mtl
way" pain is so small) that it isn't done in mtl
.
That said, you may enjoy using the transformers-base
package.
Is it just the fact that the transformers handle all "other" effects so that the only things at the very bottom are either
Identity
(already handled withreturn :: a -> m a
) orIO
?
As you say, the most common bases are IO
(for which we already have MonadIO
) or Identity
(for which one generally just uses return
and a pure computation rather than a lifted monadic computation). Sometimes ST
is a handy base monad, but it is a bit more rare to use transformers over ST
than it is to use them over IO
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With