26

我想知道如何在惰性函数式语言中实现调试。
你能使用断点、打印语句和传统技术吗?这甚至是个好主意吗?
我的理解是纯函数式编程不允许副作用,除了 monad。
执行顺序也不能保证。
您是否必须为要测试的每一段代码编写一个 monad?我想从这个领域更有经验的人那里得到一些关于这个问题的见解。

4

5 回答 5

31

没有什么能阻止您在延迟评估的函数式程序中使用断点。急切求值的不同之处在于程序何时会在断点处停止以及跟踪的外观。当设置断点的表达式实际上正在减少时(显然),程序将停止。

而不是你习惯的堆栈跟踪,你得到的减少导致了带有断点的表达式的减少。

愚蠢的小例子。你有这个 Haskell 程序。

add_two x = 2 + x

times_two x = 2 * x

foo = times_two (add_two 42)

然后在第一行 ( ) 放置一个断点add_two,然后评估foo。当程序在断点处停止时,在一种急切的语言中,您希望有类似的跟踪

add_two
foo

甚至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( times_two-2) 中的评估确实强制(add_two 42)从行中评估 (-1) foo。从那里您可以像在命令式调试器中一样执行一个步骤(执行下一个缩减)。

使用 Eager 语言进行调试的另一个区别是变量可能尚未评估 thunk。例如,在上述 trace 和 inspect 的第 -2 步x,你会发现它仍然是一个未评估的 thunk(在 GHCi 中用括号表示)。

有关更详细的信息和示例(如何单步执行跟踪、检查值等),请参阅GHC 手册中的 GHCi 调试器部分。还有Leksah IDE,因为我是 VIM 和终端用户,所以我还没有使用过,但根据手册,它有一个 GHCi 调试器的图形前端。

您还要求打印报表。只有使用纯函数,这并不容易,因为打印语句必须在 IO monad 中。所以,你有一个纯函数

foo :: Int -> Int

并希望添加一个跟踪语句,打印将在 IO monad 中返回一个动作,因此您必须调整要放入​​该跟踪语句的函数的签名,以及调用它的函数的签名, ...

这不是一个好主意。因此,您需要某种方式来打破纯度以实现跟踪语句。在 Haskell 中,这可以通过unsafePerformIO. 有Debug.Trace已经有功能的模块

trace :: String -> a -> a

它输出字符串并返回第二个参数。写成纯函数是不可能的(好吧,如果你打算真正输出字符串,那就是)。它unsafePerformIO在引擎盖下使用。您可以将其放入纯函数中以输出跟踪打印。

您是否必须为要测试的每一段代码编写一个 monad?

我建议相反,使尽可能多的函数成为纯函数(我在这里假设您的意思是用于打印的 IO monad,monad 不一定是不纯的)。惰性评估允许您非常干净地将 IO 代码与处理代码分开。

命令式调试技术是否是一个好主意取决于情况(像往常一样)。我发现使用 QuickCheck/SmallCheck 进行测试比使用命令式语言进行单元测试更有用,所以我会先走这条路,以避免尽可能多的调试。QuickCheck 属性实际上做出了简洁明了的函数规范(命令式语言中的许多测试代码对我来说就像是另一块代码)。

避免大量调试的一个技巧是将函数分解为许多较小的子函数并尽可能多地测试它们。当来自命令式编程时,这可能有点不寻常,但无论您使用哪种语言,这都是一个好习惯。

再说一次,调试!= 测试,如果某处出现问题,断点和跟踪可能会帮助你。

于 2009-08-24T21:41:56.910 回答
6

我不认为这个话题可以在短时间内处理。请阅读以下链接中提供的论文:

  1. 跟踪纯功能程序的理论
  2. Haskell Tracer 出版物
  3. Haskell 调试技术
于 2009-08-20T03:44:33.770 回答
2

我从来没有深入研究过 Haskell 中任何非常复杂的东西,但是副作用几乎消失的事实已经消除了大部分调试需求。纯函数在没有调试器的情况下测试和验证非常简单。

另一方面,我确实经历过几次我需要在 monad 中调试某些东西,在这种情况下我已经能够打印/记录/任何东西。

至少对于较小的程序或系统来说,调试有点过时了。强类型和静态类型检查确实进一步消除了您在过程编程中发现的传统错误。大多数错误(如果有的话)都是逻辑错误(称为错误函数、数学错误等)——非常容易交互测试。

于 2009-08-20T04:07:03.447 回答
2

根据Clojure的经验(它是惰性的、功能性的、鼓励但不强制执行纯洁性):

  • 您可以像使用任何其他语言一样设置断点。但是,由于惰性评估,这些可能不会立即被调用,但会在评估惰性结构被强制时立即被命中。

  • 在允许副作用的惰性函数语言(包括 Clojure)中,您可以相对容易地插入 printlns 和其他调试日志记录。我个人觉得这些非常有用。您必须小心何时因为懒惰而调用它们,但是如果您根本看不到输出,则可能暗示您的代码由于懒惰而没有被评估.....

说了这么多,到目前为止,我从来不需要求助于调试器。通常一些简单的测试(可能在 REPL 上)就足以验证功能代码是否正常工作,如果这些测试失败,那么通常很明显出了什么问题。

于 2011-04-04T12:41:03.770 回答
0

请允许我宣传我自己的工具来调试懒惰问题。它帮助我在一小时内解决了我已经花了 2 天时间调试的与惰性相关的内存泄漏。

http://www.haskell.org/pipermail/haskell-cafe/2012-January/098847.html

http://hackage.haskell.org/package/htrace

于 2012-10-19T20:21:54.827 回答