when I went through the last chapter of LYAH and met with ListZipper, I gave myself an assignment that to make it a State monad so that the source code would look more clear like:
manipList = do
goForward
goForward
goBack
and at the same time, I wanted to keep a log for this process by taking advantage of Writer monad, but I didn't know how to combine these two Monads together.
My solution was to keep a [String] inside the state, and my source code is
import Control.Monad
import Control.Monad.State
type ListZipper a = ([a], [a])
-- move focus forward, put previous root into breadcrumbs
goForward :: ListZipper a -> ListZipper a
goForward (x:xs, bs) = (xs, x:bs)
-- move focus back, restore previous root from breadcrumbs
goBack :: ListZipper a -> ListZipper a
goBack (xs, b:bs) = (b:xs, bs)
-- wrap goForward so it becomes a State
goForwardM :: State (ListZipper a) [a]
goForwardM = state stateTrans where
stateTrans z = (fst newZ, newZ) where
newZ = goForward z
-- wrap goBack so it becomes a State
goBackM :: State (ListZipper a) [a]
goBackM = state stateTrans where
stateTrans z = (fst newZ, newZ) where
newZ = goBack z
-- here I have tried to combine State with something like a Writer
-- so that I kept an extra [String] and add logs to it manually
-- nothing but write out current focus
printLog :: Show a => State (ListZipper a, [String]) [a]
printLog = state $ \(z, logs) -> (fst z, (z, ("print current focus: " ++ (show $ fst z)):logs))
-- wrap goForward and record this move
goForwardLog :: Show a => State (ListZipper a, [String]) [a]
goForwardLog = state stateTrans where
stateTrans (z, logs) = (fst newZ, (newZ, newLog:logs)) where
newZ = goForward z
newLog = "go forward, current focus: " ++ (show $ fst newZ)
-- wrap goBack and record this move
goBackLog :: Show a => State (ListZipper a, [String]) [a]
goBackLog = state stateTrans where
stateTrans (z, logs) = (fst newZ, (newZ, newLog:logs)) where
newZ = goBack z
newLog = "go back, current focus: " ++ (show $ fst newZ)
-- return
listZipper :: [a] -> ListZipper a
listZipper xs = (xs, [])
-- return
stateZipper :: [a] -> (ListZipper a, [String])
stateZipper xs = (listZipper xs, [])
_performTestCase1 = do
goForwardM
goForwardM
goBackM
performTestCase1 =
putStrLn $ show $ runState _performTestCase1 (listZipper [1..4])
_performTestCase2 = do
printLog
goForwardLog
goForwardLog
goBackLog
printLog
performTestCase2 = do
let (result2, (zipper2, log2)) = runState _performTestCase2 $ stateZipper [1..4]
putStrLn $ "Result: " ++ (show result2)
putStrLn $ "Zipper: " ++ (show zipper2)
putStrLn "Logs are: "
mapM_ putStrLn (reverse log2)
But the problem is that I don't think this is a good solution since I have to maintain my logs manually. Is there any alternative way of mixing State monad and Writer monad so that they could work together?
Tikhon Jelvis gives a nice answer with monad transformers. However, there is a quick solution also.
The Control.Monad.RWS
module in mtl
exports the RWS
monad, which is a combination of the Reader
, Writer
and State
monads.
You're looking for monad transformers. The basic idea is to define a type like WriterT
which takes another monad and combines it with a Writer
creating a new type (like WriterT log (State s)
).
Note: there is a convention that transformer types end with a capital T
. So Maybe
and Writer
are normal monads and MaybeT
and WriterT
are their transformer equivalents.
The core idea is very simple: for a bunch of monads, you can easily imagine combining their behavior on bind. The simplest example is Maybe
. Recall that all that Maybe
does is propagate Nothing
on bind:
Nothing >>= f = Nothing
Just x >>= f = f x
So it should be easy to imagine extending any monad with this behavior. All we do is check for Nothing
first and then use the old monad's bind. The MaybeT
type does exactly this: it wraps an existing monad and prefaces each bind with a check like this. You would also have to implement return
by essentially wrapping the value in a Just
and then using the inner monad's return
. There is also a bit more plumbing to get everything to work, but this is the important idea.
You can imagine a very similar behavior for Writer
: first we combine any new output then we use the old monad's bind. This is essentially the behavior of WriterT
. There are some other details involved, but the basic idea is fairly simple and useful.
Monad transformers are a very common way to "combine" monads like you want. There are versions of most commonly used monads as transformers, with the notable exception of IO
which always has to be at the base of your monad stack. In your case, both WriterT
and StateT
exist and could be used for your program.
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