Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how could I take advantage of both State and Writer in haskell?

Tags:

haskell

monads

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?

like image 944
Javran Avatar asked Sep 08 '12 08:09

Javran


2 Answers

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.

like image 189
opqdonut Avatar answered Nov 16 '22 07:11

opqdonut


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.

like image 17
Tikhon Jelvis Avatar answered Nov 16 '22 07:11

Tikhon Jelvis