4

所以我正在构建一个 State Monad,并遇到了一些问题,因为它的懒惰特性让我很难调试。

我的 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,"

但最终的结果是一样的。有没有更好的方法来处理状态单子在调试中的懒惰,所以它更自然地顺序?

4

2 回答 2

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是对先前 push 的调用trace,因此 trace 需要首先运行,等等。因此,tracing 输出中的数据依赖关系确保 trace 以 push 的顺序运行。

为了确保在没有数据依赖的情况下以该顺序运行跟踪,您必须将trace调用拉出put,或强制评估put状态,

put $! trace ((show $ length lst) ++ " " ++ (show cur)) ((val) ++ "," ++ lst)

或者

trace ((show $ length lst) ++ " " ++ (show cur)) $ put ((val) ++ "," ++ lst)
于 2012-10-29T19:26:57.307 回答
0
  1. 这是一个单子定律m >>= return === m。因此,您可以return =<<离开runSim []
  2. 您的最终结果是错误的,因为您将当前值添加到状态,但您想要的是附加。更改((val) ++ "," ++ lst)(lst ++ "," ++ val),您的输出就是您所期望的:",2,4,6,8,10,12,14,16,18,20". (稍后您可以担心前导逗号。)
  3. state monad 用于读取和写入状态。在您的情况下,您要做的就是编写,这就是Writermonad 的用途:

    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更多地用作查看程序内部的手提钻方式,而不是推理它是如何做的。
于 2012-10-29T17:51:47.003 回答