Question

I ran across the RWS Monad and its MonadTransformer while looking up something in the mtl library. There is no real documentation there, and I was wondering what this is and where it gets used.

I've gotten as far as finding that RWS is an acronym fro Reader, Writer, State and that is is a stack of those three monad transformers. What I cannot figure out is why this is better then State by itself.

Was it helpful?

Solution

The most practical reason for this is for testability and for more precise type signatures.

The key strength of haskell is how well you can specify what a function does through the type system. Compare the c#/java type:

public int CSharpFunction(int param) { ...

with a haskell one:

someFunction :: Int -> Int

The haskell one not only tells us the types needed for parameters and the return type, but also what the function may affect. For example, it can't do any IO, nor can it read or change any global state, or access any static configuration data. Neither may be true for the c# function, but we cannot tell.

This is a great help with testing. We know that the only things we need to test with the haskell someFunction is if it gets the expected outputs for some sample inputs. There isn't any possible setup required, and the function will never give a different result for the same input. This makes testing pretty simple with pure functions.


However, often a function cannot be pure. For example, it may need to access some global information just for reading. We could just add another parameter to the function:

readerFunc :: GlobalConfig -> Int -> Int

But it is often easier to use a monad, since they are easier to compose:

readerFunc2 :: Int -> Reader GlobalConfig Int

Testing this is almost as easy as testing a pure function. We just need to test various permutations of the input Int value, and the GlobalConfig reader configuration value.

A function may need to write out values (eg for logging). This can also be done with a monad:

writerFunc :: Int -> Writer String Int

Again testing this is almost as easy as for a pure function. We just test if for a given Int input, the appropriate Int is returned, as well as the right final writer String.

Another function may need to read and change state:

stateFunc :: Int -> State GlobalState Int

This is harder to test though. Not only do we have to check the output using various input Ints and initial GlobalStates, but we also need to test if the final GlobalState is the correct value.


Now consider a function which:

  • Needs to read from a ProgramConfig data type
  • Write values to a string for logging
  • Use and modify a ProgramState value.

We could do something like this:

data ProgramData = ProgramData { pState :: ProgramState, pConfig :: ProgramConfig, pLogs :: String }
complexFunction :: Int -> State ProgramData Int

However, that type isn't very accurate. It implies that the ProgramConfig may be changed, which it won't. It also implies that the function may use the pLogs value, which it won't. Also, testing it is more complex, as theoretically, the function could accidently change the program config, or use the current pLogs value in its computations.

This can be greatly improved upon with this:

betterFunction :: Int -> RWS ProgramConfig String ProgramState Int

This type is very accurate. We know that the ProgramConfig is only ever read from. We know the String is only ever changed. The only part that requires both reading and writing is the ProgramState, as expected. This is easier to test, as we only need to test different combinations of ProgramConfig, ProgramState and Int for input, and check the output Int, String and ProgramState for output. We know we cannot accidently change the program config, or use the current program log value in our computations.


The haskell type system is there to help us, we should give it as much information as we can so it can catch errors before we do.

(Note that every haskell type in this answer could be the equivalent to the c# type at the top, depending on how the c# function is actually implemented).

OTHER TIPS

State is a very generalized concept and hence can be used for many things. Reader and Writer can be thought of a specialized form of State with some constrains, you can only read from a reader and you can only write to a writer. Using these specialized form of state you can be more explicit about what you are trying to achieve or what exactly are the intentions.

Another analogy could be using map/dictionary to model anything (objects, data, event handlers etc), but using a more specialized form of map/dictionary makes things more explicit

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top