6

When I design my programming model I always have a dilemma which approach is better:

type MyMonad1 = StateT MyState (Reader Env)
type MyMonad2 = ReaderT Env (State MyState)

What are the benefits and trade offs between using one monad stack over another? Does it matter at all? What about performance?

radrow
  • 6,419
  • 4
  • 26
  • 53
  • 4
    I'm no expert in Monad Transformers, but as far as I can tell it doesn't really matter here. Modulo newtypes, `MyMonad1 a` is the same as `MyState -> Env -> (a, MyState)`,while `MyMonad2 a` is `Env -> MyState -> (a, MyState)`, so the only difference is argument order. – Robin Zigmond Jun 02 '19 at 13:21
  • 2
    Is this question specifically about `ReaderT` and `StateT`, or is it generally about how to choose the order of transformers in a stack? – Daniel Wagner Jun 02 '19 at 13:54
  • Specifically about these two – radrow Jun 02 '19 at 15:11

1 Answers1

8

In the general case, different orderings of monad transformers will lead to different behaviors, but as was pointed out in the comments, for the two orderings of "state" and "reader", we have the following isomorphisms up to newtypes:

StateT MyState (Reader Env) a  ~  MyState -> Env -> (a, MyState)
ReaderT Env (State MyState) a  ~  Env -> MyState -> (a, MyState)

so the only difference is one of argument order, and these two monads are otherwise semantically equivalent.

With respect to performance, it's hard to know for sure without benchmarking actual code. However, as one data point, if you consider the following monadic action:

foo :: StateT Double (Reader Int) Int
foo = do
  n <- ask
  modify (* fromIntegral n)
  gets floor

then when compiled with GHC 8.6.4 using -O2, the newtypes are -- obviously -- optimized away, and this generates exactly the same Core if you change the signature to:

foo :: ReaderT Int (State Double) Int

except that the two arguments to foo get flipped. So, there's no performance difference at all, at least in this simple example.

Stylistically, you might run into situations where one ordering leads to nicer looking code than the other, but usually there won't be much to choose between them. In particular, basic monadic actions like the one above will look exactly the same with either ordering.

For no good reason, I tend to favor #2, mostly because the Env -> MyState -> (a, MyState) looks more natural to me.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • 2
    A tiny quibble: simply having two isomorphic types isn't really enough when we're talking about erasing newtypes; one also has to check that the associated instances respect the isomorphism. (For example, consider `All` and a hypothetical `Xor`, which both unwrap to `Bool`, but whose `Monoid` instances do not respect either the `id` or the `not` isomorphism.) It's a very minor quibble in this case, though, since the instances do indeed correspond appropriately. – Daniel Wagner Jun 03 '19 at 21:10