没有什么可以阻止您在延迟评估的函数程序中使用断点。与急切评估的区别是when程序将在断点处停止并且跟踪将是什么样子。当设置断点的表达式实际上被减少时(显然),程序将停止。
您得到的不是您习惯的堆栈跟踪,而是导致带有断点的表达式的减少。
小愚蠢的例子。你有这个 Haskell 程序。
add_two x = 2 + x
times_two x = 2 * x
foo = times_two (add_two 42)
然后你在第一行放置一个断点(add_two
),然后评估foo
。当程序在断点处停止时,用一种热切的语言,您会期望有一个类似的跟踪
add_two
foo
and times_two
甚至还没有开始评估,但在 GHCi 调试器中你会得到
-1 : foo (debug.hs:5:17-26)
-2 : times_two (debug.hs:3:14-18)
-3 : times_two (debug.hs:3:0-18)
-4 : foo (debug.hs:5:6-27)
<end of history>
这是导致您放置断点的表达式减少的减少列表。请注意,它看起来像times_two
“叫”foo
尽管它没有明确这样做。从这里可以看出评价2 * x
in times_two
(-2) 确实强制评估(add_two 42)
(-1) 从foo
线。从那里您可以像在命令式调试器中一样执行一个步骤(执行下一个减少)。
使用 eager 语言进行调试的另一个区别是变量可能尚未被评估 thunk。例如,在上面的跟踪和检查中的步骤-2x
,你会发现它仍然是一个未评估的 thunk(在 GHCi 中用括号表示)。
有关更详细的信息和示例(如何单步执行跟踪、检查值……),请参阅GHCi 调试器部分 http://www.haskell.org/ghc/docs/latest/html/users_guide/ghci-debugger.html在 GHC 手册中。还有莱克萨IDE http://leksah.org/由于我是 VIM 和终端用户,所以我还没有使用它,但根据手册,它有一个 GHCi 调试器的图形前端。
您还要求打印报表。仅对于纯函数,这不太容易实现,因为 print 语句必须位于 IO monad 内。所以,你有一个纯函数
foo :: Int -> Int
并且希望添加跟踪语句,打印将在 IO monad 中返回一个操作,因此您必须调整要放入该跟踪语句的函数的签名以及调用它的函数的签名, ...
这不是一个好主意。因此,您需要某种方法来打破纯度来实现跟踪语句。在 Haskell 中,这可以通过以下方式完成unsafePerformIO
。有的是Debug.Trace http://www.haskell.org/ghc/docs/latest/html/libraries/base/Debug-Trace.html已经有功能的模块
trace :: String -> a -> a
它输出字符串并返回第二个参数。不可能将其编写为纯函数(好吧,如果您打算真正输出字符串)。它用unsafePerformIO
在引擎盖下。您可以将其放入纯函数中以输出跟踪打印。
您是否必须为要测试的每个代码部分编写一个 monad?
我建议相反,使尽可能多的函数变得纯净(我假设这里你指的是用于打印的 IO monad,monad 不一定是不纯净的)。惰性求值允许您非常干净地将 IO 代码与处理代码分开。
命令式调试技术是否是一个好主意取决于具体情况(像往常一样)。我发现使用 QuickCheck/SmallCheck 进行测试比命令式语言中的单元测试有用得多,因此我会首先采用这条路线,以避免尽可能多的调试。 QuickCheck 属性实际上制定了简洁的函数规范(命令式语言中的许多测试代码对我来说看起来只是另一块代码)。
避免大量调试的一个技巧是将函数分解为许多较小的子函数并测试尽可能多的子函数。当来自命令式编程时,这可能有点不寻常,但无论您使用哪种语言,这都是一个好习惯。
再说一遍,调试!=测试,如果某个地方出了问题,断点和跟踪可能会帮助您。