调试状态Monad时,根据跟踪的位置反转顺序

时间:2012-10-29 17:33:15

标签: haskell

所以我正在建立一个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在调试中的懒惰,所以它更自然地顺序?

2 个答案:

答案 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)

  1. 这是m >>= return === m的monad法律。因此,您可以将return =<<留在runSim []
  2. 您的最终结果是错误的方式,因为您将当前值添加到状态,但您想要的是追加。将((val) ++ "," ++ lst)更改为(lst ++ "," ++ val),您的输出符合预期:",2,4,6,8,10,12,14,16,18,20"。 (你可以稍后担心领先的逗号。)
  3. 州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。)

  4. 一般注意事项:因为Haskell主要由很好的小功能组成,所以通常很容易推断单个功能的作用。您的runSim获取当前状态并计算列表头的两倍。之后,它会执行一些跟踪操作,然后将val ++ "," ++ lst作为新状态返回,然后再作为最后一步递归。关于Haskell的好处是,这是该函数唯一能做的事情:取状态,乘法,置状态。如果您理解了这一次的迭代,您将理解所有迭代。我的意思是说 - 即使在你的情况下这不是一个懒惰的问题 - 懒惰不影响你的程序的结果(事实上不能,除非你有底部可能)。 trace在中间步骤中给出了什么并不重要,如果GHC决定首先使用偶数向后移动列表然后使用奇数向前移动,结果仍然是相同的。然而trace包含邪恶的魔法,所以它可能令人困惑。我会更多地使用trace作为查看程序内部的方式,而不是推断它是如何做到的。