Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is MonadIO specific to IO, rather than a more generic MonadTrans?

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?

like image 727
CR Drost Avatar asked Jul 05 '16 20:07

CR Drost


People also ask

Why use monad transformers?

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).

What is Liftio?

A Monad that can convert any given IO[A] into a F[A] , useful for defining parametric signatures and composing monad transformer stacks.


1 Answers

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 with return :: a -> m a) or IO?

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.

like image 158
Daniel Wagner Avatar answered Nov 14 '22 06:11

Daniel Wagner