9

当我阅读 LYAH 的最后一章并遇到ListZipper时,我给自己分配了一个任务,让它成为一个 State monad,以便源代码看起来更清晰:

manipList = do
    goForward
    goForward
    goBack

同时,我想利用 Writer monad 为这个过程保留一个日志,但我不知道如何将这两个 Monad 组合在一起。

我的解决方案是在状态内保留一个 [String],我的源代码是

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)

但问题是我认为这不是一个好的解决方案,因为我必须手动维护我的日志。有没有其他方法可以混合 State monad 和 Writer monad 以便它们可以一起工作?

4

2 回答 2

17

您正在寻找monad 转换器。基本思想是定义一个 like 类型WriterT,它接受另一个 monad 并将其与Writer创建新类型 (like WriterT log (State s)) 结合起来。

注意:有一个约定,变压器类型以大写字母结尾T。所以MaybeWriter是普通的单子,MaybeTWriterT是它们的变压器等价物。

核心思想非常简单:对于一堆 monad,你可以很容易地想象在 bind 上组合它们的行为。最简单的例子是Maybe。回想一下,所做的一切都是在绑定Maybe上传播:Nothing

Nothing >>= f = Nothing
Just x >>= f = f x

所以应该很容易想象用这种行为扩展任何monad。我们所做的只是先检查Nothing,然后使用旧的 monad 的绑定。该MaybeT类型正是这样做的:它包装了一个现有的 monad,并在每个绑定前加上这样的检查。您还必须return通过将值包装在 a 中Just然后使用内部 monad 来实现return。还有更多的管道可以让一切正常工作,但这是重要的想法。

你可以想象一个非常相似的行为Writer:首先我们组合任何新的输出,然后我们使用旧的 monad 的绑定。这本质上是 的行为WriterT。还有一些其他的细节,但基本的想法是相当简单和有用的。

Monad 转换器是一种非常常见的“组合”你想要的 monad 的方法。最常用的 monad 有多种版本作为转换器,但值得注意的例外是IO始终必须位于 monad 堆栈的底部。在您的情况下,两者都WriterT存在StateT并且可以用于您的程序。

于 2012-09-08T08:46:46.043 回答
13

Tikhon Jelvis 用 monad 转换器给出了一个很好的答案。但是,也有一个快速的解决方案。

中的Control.Monad.RWS模块mtl导出RWSmonad,它是Reader,WriterStatemonad 的组合。

于 2012-09-08T09:29:48.133 回答