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