Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to understand the types produced by monad transformers

The docs for Control.Monad.Trans.Error provide this example of combining two monads:

type ErrorWithIO e a = ErrorT e IO a
==> ErrorT (IO (Either e a))

I find this counterintuitive: even though ErrorT is supposedly wrapping IO, it looks like the error information has been injected into the IO action's result type. I would've expected it to be

==> ErrorT (Either e (IO a))

based on the usual meaning of the word "wrap".

To make matters more confusing, StateT does some of each:

type MyError e = ErrorT e Identity  -- (see footnote)
type StateWithError s e a = StateT s (MyError e) a
==> StateT (s -> ErrorT (Either e (a, s)))

The state type s has been injected into the Either's Right side, but the whole Either has also been wrapped in a function.

To make matters even more confusing, if the monads are combined the other way around:

type ErrorWithState e s a = ErrorT e (State s) a
==> ErrorT (StateT (s -> (Either e a, s)))

the "outside" is still a function; it doesn't produce something like Either e (s -> (a, s)), where the state function is nested within the error type.

I'm sure there's some underlying logical consistency to all this, but I don't quite see it. Consequently I find it difficult to think about what it means to combine one monad with another, even when I have no trouble understanding what each monad means individually.

Can someone enlighten me?


(Footnote: I'm composing ErrorT with Identity so that StateWithError and ErrorWithState are consistent with each other, for illustrative purposes. Normally I'd just use StateWithError s e a = StateT s (Either e) a and forego the ErrorT layer.

like image 554
Wyzard Avatar asked Aug 17 '11 04:08

Wyzard


2 Answers

I find this counterintuitive: even though ErrorT is supposedly wrapping IO, it looks like the error information has been injected into the IO action's result type.

Monad transformers aren't in general "wrapping" the monad they're applied to, at least not in any obvious sense. Thinking of it as "wrapping" would imply functor composition to my mind, which is specifically what's not going on here.

To illustrate, functor composition for State s and Maybe, with definitions expanded, would look like this:

newtype StateMaybe s a = StateMaybe (s -> (Maybe a, s))    -- == State s (Maybe a)
newtype MaybeState s a = MaybeState (Maybe (s -> (a, s)))  -- == Maybe (State s a)

Note that in the first case, State behaves normally, and Nothing doesn't impact the state value; in the second case, we either have a plain State function or nothing at all. In neither case do the characteristic behaviors of the two monads actually combine. This shouldn't be surprising since, after all, these are the same as what you'd get by simply having values using one monad as regular values used within the other.

Compare this to StateT s Maybe:

newtype StateTMaybe s a = StateTMaybe (s -> Maybe (a, s))

In this case, the two are woven together; things proceed in the normal manner for State, unless we hit a Nothing, in which case the computation is aborted. This is fundamentally different from the above cases, which is why monad transformers even exist in the first place--composing them naively doesn't require any special machinery, because they operate independently of each other.


As far as making sense of which one is on the "outside", it might help to think of the "outer" transformer as being the one whose behavior takes "priority", in some sense, when dealing with values in the monad, while the "inner" monad only sees business as usual. Note that this is why IO is always innermost--it doesn't let anything else get up in its business, whereas a hypothetical IOT transformer would be forced to allow the wrapped monad to pull all kinds of shenanigans, like duplicating or discarding the RealWorld token.

  • StateT and ReaderT both put the "inner" monad around the result of the function; you have to provide a state value or environment before getting at the transformed monad.

  • MaybeT and ErrorT both slip themselves inside the transformed monad, ensuring that it can behave in the usual manner except that a value that might not be present.

  • Writer is completely passive and just attaches itself to the values in the monad, since it doesn't impact behavior at all.

  • ContT keeps things to itself, putting off dealing with the transformed monad altogether by only wrapping the result type.

That's a bit hand-wavy, but eh, monad transformers are kind of ad-hoc and confusing to begin with, alas. I don't know if there's any tidy theoretical justification for the particular choices made, other than the fact that they work, and do what you'd usually want the combination (not composition) of the two monads to do.

Consequently I find it difficult to think about what it means to combine one monad with another, even when I have no trouble understanding what each monad means individually.

Yeah, this sounds like about what to expect, I'm afraid.

like image 80
C. A. McCann Avatar answered Nov 10 '22 20:11

C. A. McCann


Consider what would happen, if ErrorT were defined the way you would imagine it. How would you encode an IO action, which fails? With Either e (IO a) you cannot give a Left value, when the action fails, because at the time you reach the action it's already clear that it's a Right value – otherwise it wouldn't be an action.

With IO (Either e a) however this isn't the case. The whole thing is an IO action now and can return a Left value to indicate error. As others noted, don't think of monad transformers as wrappers. Rather think of them as functions. They take a monad and turn it into another monad. They transform monads.

like image 5
ertes Avatar answered Nov 10 '22 20:11

ertes