About the choice of where to apply the monad parameter of a monad transformer
Asked Answered
N

1

18

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:

  • What would have been wrong with it?
  • If nothing would have been flat out wrong, would have such a monad transfomer been of little help?
  • What I see wrong in it, is that it feels too unrestricted, in the sense that running such a monad transformer on top of 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 state 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?

Nissensohn answered 11/4, 2024 at 20:25 Comment(1)
conway.rutgers.edu/~ccshan/wiki/blog/posts/Monad_transformers might be interesting. It seems (wrt category theory) that monad transformers are an ad hoc collection of useful monad morphisms, with little underlying structure that unifies them in the sense of, for exapmle, how one decides where m gets inserted into the type. (Though I will note that the common ones are all of the form f ∘ m ∘ g, for functors f and g, so it doesn't appear to be entirely arbitrary where m can be inserted.)Chicky
K
21

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 state to produce the new state (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.

Katelyn answered 11/4, 2024 at 21:58 Comment(1)
It looks like these are both applicatives that aren't monads. In the first, the "structure" of the m-operations is fixed and can't be influenced by the evolution of the state. In the second, the "structure" of the state changes is fixed and cannot be influenced by the m-operations. Kind of neat...Loo

© 2022 - 2025 — McMap. All rights reserved.