Take the MaybeT
monad transformer:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
I wouldn't have expected a different definition for it, because Maybe
is just kind of a box with a (optional) content in it (of type a
above), so what could MaybeT
do with the parameter m
, if not wrappeing the whole Maybe
in it? Writing :: Maybe (m a)
would have made no sense, as that's just the type of someaction <$> theMaybe
.
But in other cases, there could have been other choices.
Take the StateT
monad transformer, defined as
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
This definition, if I look at it in light of the State
monad's definition (as it would be if it was not defined on top of StateT
via Identity
, clearly),
newtype State s a = State { runState :: s -> (a, s) }
seems to be in line with the interpretation of "a function a -> b
as a container with content b
", which is how I originally understood what's the meaning of the Functor
instance of (->) r
. So fair enough, m
is wrapping the "content" of the function (i.e. the "content" of the stateful computation).
But this is (or seems to me!) quite different form the case of MaybeT
, where m
does not wrap the inside of the Maybe
, which is a
, but the whole Maybe a
.
If StateT
had to resemble MaybeT
(in this fact of using m
to wrap the whole thing), it would have been like this
newtype StateT' s m a = StateT' { runStateT' :: m (s -> (a,s)) }
About this, some thoughts come to mind:
IO
would mean that one can pick an a whole arbitrary stateful computation, s -> (a, s)
, out of the IO
monad, whereas if the actual StateT
is stacked on top of IO
, IO
can only be used to get the new s
tate and the result a
; but still, I'm not sure I understand the difference fully.And finally, what about this?
newtype StateT'' s m a = StateT'' { runStateT'' :: s -> (m a,s) }
This feels a bit useless, because... it feels like the stateful computation makes no sense, because, again with the example of m
being IO
, a
will come from IO
, but... Oh, I really don't understand.
The point is that I have experience of the usefulness of StateT
as it is, but I don't really understand the reasons why it is useful, and other alternatives wouldn't have been. Or would they?
To consider what can go wrong, take m = IO
and think about what effects can be represented by your types, and which instead can not.
newtype StateT' s m a = StateT' { runStateT' :: m (s -> (a,s)) }
Well, this becomes IO (s -> (a,s))
, so it performs the IO effects before reading the current s
tate to produce the new s
tate (and the result a
).
So, this can not express the computation "read the current state and print it". Not that useful.
newtype StateT'' s m a = StateT'' { runStateT'' :: s -> (m a,s) }
Here we get s -> (IO a,s)
. It's easy to obtain from this a function s -> s
that computes the new state in a pure way. This means that the IO can not affect the new state.
So, this can not express the computation "ask the user for keyboard input, and change the state depending on that". Also not that useful.
On top of these concerns, we should note that the type we get by placing m
in random places could even fail to be a monad! I am not sure we can define a sensible >>=
for the types you propose. Attempting that could be a nice exercise which might lead to convincing yourself it can not really work.