Using MonadTrans
When we wrote code for our MonadState
example, we had something that looked like this:
type Output = Int
type StateType = Int
computation :: State StateType Output
computation = do
modify_ (_ + 1)
modify_ (_ * 10)
modify_ (_ + 1)
main :: Effect Unit
main = case runState computation 0 of
Tuple output state -> do
log $ "Result of computation: " <> show output
log $ "End state of computation: " <> show state
The above code works because we're using MonadState
behind the scenes via StateT
's instance. However, this function's type signature restricts us to only using StateT
for computations. If we want to define computation
, so that it can use functions from MonadWriter
, we'll need to use a different approach. Let's fix this one step at a time.
First, we'll abstract our State
type into MonadState
by using a type constraint:
type Output = Int
type StateType = Int
computation :: forall m => MonadState StateType m => m Output
computation = do
modify_ (_ + 1)
modify_ (_ * 10)
modify_ (_ + 1)
-- use a helper function to tell the type inferer that
-- `computation`'s `m` type is `StateT`
runProgram :: State StateType Output -> Tuple Output StateType
runProgram s = runState s 0
main :: Effect Unit
main = case runProgram computation of
Tuple string state -> do
log $ "Result of computation: " <> string
log $ "End state of computation: " <> show state
Second, we'll add another type class constraint for MonadAsk
to expose it's tell
function:
type Output = Int
type StateType = Int
type NonOutputData = String
computation :: forall m
. MonadState StateType m
=> MonadAsk NonOuputData m
=> m Output
computation = do
modify_ (_ + 1)
tell "Modified state by adding 1"
currentState <- modify (_ * 10)
tell $ "Modified state by multiplying by 10. It is now "
<> show currentState
modify_ (_ + 1)
Great! We now have a single computation that can do both state manipulation and use tell
. However, how does that affect runProgram
?
runProgram :: StateT_and_WriterT -> StateT_and_WriterT_Output
runProgram s = -- ???
When we used a monad (e.g. WriterT
) to run a computation, we didn't need to specify the monad type being used. So, we used Identity
as a placeholder monad and used the type alias, Writer
, to make it easier to write. To use another computational monad (e.g. StateT
) inside of Writer
, we now need to specify what that monad is by re-exposing the T
part of WriterT
and replacing Identity
with StateT
. Putting it differently, WriterT
is now transforming the monad, StateT
with additional effects, which is likewise transforming the base monad, Identity
with additional effects:
-- simple writer computation
writer :: Writer NonOutputData Output -> Tuple NonOuputData Output
writer w = runWriter w
-- re-expose the T part of WriterT
writer :: WriterT NonOutputData Identity Output -> Tuple NonOuputData Output
writer w = runWriter w
-- swap `Identity` with a type alias called Computation
type Computation = Identity
writer :: WriterT NonOutputData Computation Output -> Tuple NonOuputData Output
writer w = runWriterT w
-- Since the types will get long soon, break up the type signature
type Computation = Identity
writer :: WriterT NonOutputData Computation Output
-> Tuple NonOuputData Output
writer w = runWriterT w
-- StateT with its T exposed but set to Identity still
state :: StateT State Identity Output -> Tuple Output State
state s = runState s initialState
-- re-alias Computation to StateT
-- and use `runWriterT` instead of `runWriter`
type Computation = StateT State Identity Output
writer :: WriterT NonOutputData Computation Output
-> Tuple NonOuputData Output
writer w = runWriterT w
-- getting rid of the type alias and inlining its type
-- and rename the function's name to 'runProgram'
runProgram :: WriterT NonOutputData (StateT State Identity stateOutput) Output
-> finalOutput
runProgram ws = ???
-- Realizing that `StateT` with all three of its types specified
-- now has kind "Type" and is thus no longer a monad ("Type -> Type"),
-- we remove the `stateOutput` type to increase
-- StateT's kind from "Type" to "Type -> Type", making it a monad again
-- so that it satisfies WriterT's monadic type requirement
runProgram :: WriterT NonOutputData (StateT State Identity) Output
-> finalOutput
runProgram ws = ???
-- To run StateT, we also need an `initialState` argument. Let's add it
runProgram :: WriterT NonOutputData (StateT State Identity) Output
-> State
-> finalOutput
runProgram ws initialState = ???
A few questions arise as we do this:
- What should
finalOutput
's type be if we combine the two monad transformers together? - How should
program
's body be implemented?
The types give us a few clues for a top-down explanation. First (answering question 2), we realize that State
's monad type is still Identity
since no other monad type is inside of StateT
. Thus, we know that we'll need to use runState
to unpack its results. Using this line of reasoning, we'll also need to use runWriterT
instead of runWriter
because the WriterT
type is using a non-Identity
monad.
That leaves us with two possible options:
runWriterT (runState ws initialState)
runState (runWriterT ws) initialState
Second (answering question 1), we know that running a StateT
returns Tuple stateOutput state
and running a WriterT
returns Tuple writerOutput nonOutputData
. That means we'll likely get something close to one of these options:
- Both outputs are returned using a Tuple that groups them together:
Tuple (Tuple stateOutput state) (Tuple writerOutput nonOutputData)
(or vice versa in its order)
- The output of one monad is the
stateOutput
(a) orwriterOutput
(b) of the other:- a:
Tuple (Tuple writerOutput nonOutputData) state
- b:
Tuple (Tuple stateOutput state ) nonOutputData
- a:
Since we're running one monad inside of another, the second option seems more likely.
The question is, which one is correct?
Let's continue by examining runStateT
/runState
and runWriterT
/runWriter
. We know that runMonad
is just a wrapper around runMonadT
when the monad is Identity
:
newtype StateT s m a =
StateT (s -> m (Tuple a s))
runState :: StateT state Identity output -> state -> Tuple output state
runState s initialState = unwrapIdentity $ runStateT s initialState
runStateT :: StateT state monad output -> state -> monad Tuple output state
runStateT (StateT function) initialState = function initialState
newtype WriterT w m a =
WriterT (m (Tuple a w))
runWriter :: WriterT nonOutputData Identity output -> Tuple output nonOutputData
runWriter w = unwrapIdentity $ runWriterT w
runWriterT :: WriterT nonOutputData monad output -> monad Tuple output nonOutputData
runWriterT w = w
They key takeaway here is that run[Monad]T
returns the same monad that is specified in [Monad]T
. Looking at our function again...
runProgram :: WriterT NonOutputData (StateT State Identity) Output
-> State
-> finalOutput
runProgram ws initialState = ???
... running the WriterT NonOuputData monad output
via runWriterT
will return its monad
type. Since that monad type is StateT State Identity Output
, we will take the output of running WriterT
(which outputs a StateT
) and run the output via runState
since StateT
's monad type is Identity
:
runProgram :: WriterT NonOutputData (StateT State Identity) Output
-> finalOutput
runProgram ws = runState (runWriterT ws) initialState
That answers the second question (how to implement runProgram
), but it still leaves us wondering what finalOutput
is. This is easier to determine if we just look at runState
and runWriterT
again:
runWriterT :: WriterT nonOutputData monad output -> monad Tuple output nonOutputData
-- reducing `monad Tuple output nonOutputData` to something easier, `m a`
type A = Tuple output nonOutputData
runWriterT :: WriterT nonOutputData monad output -> monad A
-- specializing `monad` to `StateT State identity`
type A = Tuple output nonOutputData
runWriterT :: WriterT nonOutputData (StateT state Identity) output
-> (StateT state Identity A)
-- re-exposing the `a` in `StateT`
runWriterT :: WriterT nonOutputData (StateT state Identity) output
-> (StateT state Identity (Tuple output nonOutputData))
-- Looking at what `runState` returns, we see
runState :: StateT state Identity output -> state -> Tuple output state
-- Replacing StateT's `output` type with `Tuple output NonOuputData`
-- we get this:
runState :: StateT state Identity (Tuple writerOutput nonOutputData)
-> state
-> Tuple (Tuple writerOutput nonOutputData) state
-- Thus, runProgram's type signature is:
runProgram :: WriterT NonOutputData (StateT State Identity Output) Output
-> State
-> Tuple (Tuple output NonOuputData) State
runProgram ws initialState =
runState (runWriterT ws) initialState
-- hidng `StateT`'s monad type, `Identity`, gets us this:
runProgram :: WriterT NonOutputData (State state) Output
-> state
-> Tuple (Tuple Output NonOuputData) state
runProgram ws initialState =
runState (runWriterT ws) initialState
Reordering the Monad Stack
What happens, however, if we flip the order of the stack? We'll see that the arguments get flipped and the output gets flipped.
runProgram :: StateT state (Writer NonOutputData) Output
-> state
-> Tuple (Tuple Output state) NonOuputData
runProgram computation initialState =
runWriter (runStateT computation initialState)
While the computation's definition did not change, how the code gets run does change.
This creates one problem with "monad stacks:" the order of how the monad transformers are run can change how the computation is evaluated. We'll cover this in more detail later.