Drawbacks of MTL
The following lists some of the issues one will face when using MTL.
Note:
- Some of the below flaws may be specific only to Haskell and not Purescript.
- In the below sources, any mention of Haskell's
IORef
is equivalent to Purescript'sRef
: a global mutable variable.
MonadState
Allows Only One State Manipulation Type
First, due to the functional dependency from m
to s
in MonadState
's definition, it's impossible to do two different state manipulations within the same function. For example...
f :: forall m ouput.
=> MonadState Int m
=> MonadState String m
-> m output
f = do
whichValue <- get
The compiler will complain because it doesn't know which value it should 'get'. See the answer to Haskell -- Chaining two states using StateT monad transformer
One solution to this is to store all states in one larger state type and then use a Lens
to access/change it:
type IntAndString = { i :: Int, s :: String }
f :: forall m output.
=> MonadState IntAndString m
-> m output
The second solution is to use type-level programming to specify which MonadState
we are referring to via an id
Symbol. This would force us to change MonadState
's definition to:
class (Monad m) <= MonadState (id :: Symbol) state m | m -> state
state :: forall a. Proxy id -> (s -> m (Tuple a s)) -> m a
_i :: Proxy "i"
_i = Proxy
_s :: Proxy "s"
_s = Proxy
f :: forall m ouput.
=> MonadState "i" Int m
=> MonadState "s" String m
-> m output
f = do
theInt <- get _i
theString <- get _s
However, I'm not sure what are the pros/cons of this approach, but this is similar to how Run
(explained in the Free
folder) enables two different state manipulations.
MonadState
& MonadWriter
lose their state on a runtime error
If a runtime error occurs in a computation that uses MonadState
or MonadWriter
, then the states in both MonadState
and MonadWriter
are lost (because the computation halts).
WriterT
& RWST
has a "space leak" problem
This is largely due to WriterT
's usage of Monoid
. The 'fix' is to drop some of its features and use a StateT
instead. See Writer Monads and Space Leaks - Infinite Negative Utility
Since RWST
also encodes things via WriterT
, it also suffers from this problem.
N-squared-ish Monad Transformer Instances
Whenever one wants to define a new monad transformer (e.g. MonadAuthenticate
) to encode some effect, one must define ~n^2
instances:
- 1
MonadAuthenticate
instance for each[Word]T
type viaMonadTrans
to lift the monadic newtypedAuthenticateT
function.
-- Given this stack of monad transformers
runCode :: AuthenticateT Credentials (StateT state (ReaderT value Identity Unit))
-- Each monadic function type (e.g. StateT, ReaderT, etc.) must
-- have an instance for MonadAuthenticate so it can lift the
-- AuthenticateT computation into the next monad.
- n instances for the monadic newtyped
AuthenticateT
function, so that it can lift its computation into all the other monad transformer type classes (e.g.AuthenticateT
->MonadState
,MonadWriter
, etc.)
-- Given this stack of monad transformers
runCode :: ReaderT Value (StateT state (AuthenticateT Credentials Identity Unit))
-- AuthenticateT must lift ReaderT and StateT into an AuthenticateT
-- monadic type.
In short, we define that many instances so that the order of the monad stack does not matter as much. If our stack has an ExceptT
somewhere in there, where that type occurs will change the final output.
Note: I say roughly ~n^2
because apparently there are some cases where "lifting" a function would break a law (or something).
Monad transformer stacks' type signatures get complicated quickly
Related to the previous point, but the type signatures start getting crazy very quickly. For new beginners who are just learning about monad transformers, this can be quite offsetting:
-- as an example using pseudo-syntax...
f :: StateT State (ReaderT reader (WriterT writer (ExceptT error Effect output) output))
The Order of the Monad Transformer Stack Matters
We mentioned this previously when covering how to use a monad transformer:
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)
-- Both `program1` and `program2` support the necessary
-- capabilities to run `computation`.
runProgram1 :: WriterT NonOutputData (State state) Output
-> state
-> Tuple (Tuple Output NonOuputData) state
runProgram1 initialState =
runState (runWriterT computation) initialState
runProgram2 :: StateT state (Writer NonOutputData) Output
-> state
-> Tuple (Tuple Output state) NonOuputData
runProgram2 initialState =
runWriter (runStateT computation initialState)
Imagine if one of these was ExceptT
. That monad transformer's location in the stack can affect how the computation works and whether it works as expected.