I am looking for resources discussing good practice for composing monads. My most pressing problem is that I'm writing a system that is making use of a series of state monads over different state types, and it seems as if the best way to handle this situation is to just create one big product type (perhaps prettied up as a record/class) encompassing all the components I'm interested in even though phase 1 is not interested in component B, and phase 2 is only interested in component A.1.
I'd appreciate pointers to nice discussions of alternatives when writing code in this sort of area. My own code-base is in Scala, but I'm happy to read discussions about the same issues in Haskell.
It's a bit challenging to use stacks of StateT
because it becomes confusing as to which layer you're talking to when you write get
or put
. If you use an explicit stack in transformers
style then you have to use a bunch of lift
s and if you use the class-based method of mtl
you get stuck entirely.
-- using transformers explicit stack style
type Z a = StateT Int (StateT String IO) a
go :: Z ()
go = do int <- get
str <- lift get
replicateM int (liftIO $ putStrLn str)
We might want to avoid that mess with an explicit product type of states. Since we end up with functions from our product state to each individual component it's easy to get
into those individual components using gets
data ZState = ZState { int :: Int, str :: String }
type Z a = StateT ZState IO a
go :: Z ()
go = do i <- gets int
s <- gets str
replicateM i (liftIO $ putStrLn s)
But this might be considered ugly still for two reasons: (1) put
and modification in general doesn't have anywhere nearly as nice a story and (2) we can't easily look at the type of a function which only impacts, say, the int
state and know that it doesn't touch str
. We'd much prefer to maintain that type-ensured modularity.
If you're lens
-savvy there's a solution called zoom
-- the real type is MUCH more general
zoom :: Lens' mother child -> StateT child m a -> StateT mother m a
which "lifts" a stateful computation on a subpart of some larger state space up to the entire state space. Or, pragmatically, we use it like this:
data ZState = ZState { _int :: Int, _str :: String }
makeLenses ''ZState
type Z = StateT ZState IO a
inc :: MonadState Int m => m ()
inc = modify (+1)
yell :: MonadState String m => m ()
yell = modify (map toUpper)
go :: Z ()
go = do zoom int $ do inc
inc
inc
zoom str yell
i <- use int
s <- use str
replicateM i (liftIO $ putStrLn s)
And now most of the problems should have vanished—we can zoom
in to isolate stateful operations which only depend upon a subset of the total state like inc
and yell
and determine their isolation in their type. We also can still get
inner state components using use
.
More than this, zoom
can be used to zoom
in on state buried deep inside various tranformer stacks. The fully general type works just fine in a situation like this
type Z a = EitherT String (ListT (StateT ZState IO)) a
>>> :t zoom int :: EitherT String (ListT (StateT Int IO)) a -> Z a
zoom int :: EitherT String (ListT (StateT Int IO)) a -> Z a
But while this is really nice, the fully general zoom
requires some heavy trickery and you can only zoom over some transformer layers. (It's today heavily unclear to me how you add that functionality to your own layer, though presumably it's possible.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With