所以我正在建立一个State Monad并遇到一些懒惰的问题,这让我很难调试。
My State monad通过获取值列表进行操作,将它们逐个推送到州的一部分,然后我分析每个州之后的状态值,以生成州的其他部分。
我想出了这个简单的例子来说明为什么它很难调试。
module Main where
import Control.Monad.State
import Debug.Trace
runSim :: [Int] -> State String String
runSim [] = return =<< get
runSim (cur:xs) = do
lst <- get
let val = (show $ 2*cur)
put $ trace ((show $ length lst) ++ " " ++ (show cur)) ((val) ++ "," ++ lst)
runSim xs
main :: IO ()
main = print $ evalState (runSim [1..10]) ""
这个输出是:
0 1
2 2
4 3
6 4
8 5
11 6
14 7
17 8
20 9
23 10
"20,18,16,14,12,10,8,6,4,2,"
但是,如果我将跟踪线更改为:
put $ trace ((show cur)) ((val) ++ "," ++ lst)
输出反转:
10
9
8
7
6
5
4
3
2
1
"20,18,16,14,12,10,8,6,4,2,"
但最终结果是一样的。有没有更好的方法来处理状态Monad在调试中的懒惰,所以它更自然地顺序?
答案 0 :(得分:8)
问题是trace
调用仅在最后进行评估。
计算建立状态(为了简洁起见仅列出两个元素)
runSim [1, 2] "" ~> ( (), state1@(trace (output 1 "") (logString 1 "")))
~> runSim [2] ( (), trace (output 2 state1) (logString2 state1))
所以在最终状态中,最后一个推送列表元素的trace
是最外层的。
现在在第二种情况下,
output i _ = show i
跟踪输出不依赖于之前发生的事情,因此最后推送的trace
会先运行等。
但在第一种情况下,
output i state = show (length state) ++ " " ++ show i
跟踪输出取决于状态,因此必须在打印跟踪输出之前评估状态。但是state
是对先前推送的trace
的调用,因此跟踪需要首先运行,等等。因此跟踪输出中的数据依赖性确保跟踪按推送的顺序运行。
要确保跟踪按顺序运行而没有数据依赖性,您必须从trace
中取出put
调用,或强制评估put
状态,< / p>
put $! trace ((show $ length lst) ++ " " ++ (show cur)) ((val) ++ "," ++ lst)
或
trace ((show $ length lst) ++ " " ++ (show cur)) $ put ((val) ++ "," ++ lst)
答案 1 :(得分:0)
m >>= return === m
的monad法律。因此,您可以将return =<<
留在runSim []
。((val) ++ "," ++ lst)
更改为(lst ++ "," ++ val)
,您的输出符合预期:",2,4,6,8,10,12,14,16,18,20"
。 (你可以稍后担心领先的逗号。)州monad用于读写状态。在你的情况下,你想要做的就是写,这就是Writer
monad的用途:
import Control.Monad.Writer
import Data.List (intercalate)
main = putStrLn . intercalate ", " . map show $ execWriter (runSim [1..10])
runSim :: [Int] -> Writer [Int] ()
runSim [] = return ()
runSim (x:xs) = tell [2*x] >> runSim xs
==> "2, 4, 6, 8, 10, 12, 14, 16, 18, 20"
(请注意,当书面列表变长时,Writer []
可能非常无效。如果您正在撰写大量内容,请使用DList
。)
runSim
获取当前状态并计算列表头的两倍。之后,它会执行一些跟踪操作,然后将val ++ "," ++ lst
作为新状态返回,然后再作为最后一步递归。关于Haskell的好处是,这是该函数唯一能做的事情:取状态,乘法,置状态。如果您理解了这一次的迭代,您将理解所有迭代。我的意思是说 - 即使在你的情况下这不是一个懒惰的问题 - 懒惰不影响你的程序的结果(事实上不能,除非你有底部可能)。 trace
在中间步骤中给出了什么并不重要,如果GHC决定首先使用偶数向后移动列表然后使用奇数向前移动,结果仍然是相同的。然而trace
包含邪恶的魔法,所以它可能令人困惑。我会更多地使用trace
作为查看程序内部的方式,而不是推断它是如何做到的。