【发布时间】:2010-11-21 04:29:17
【问题描述】:
我想知道如何在惰性函数式语言中实现调试。
你能使用断点、打印语句和传统技术吗?这是一个好主意吗?
据我了解,纯函数式编程不允许副作用,除了 monad。
执行顺序也无法保证。
您是否必须为要测试的每一段代码编写一个 monad?
我想从这个领域更有经验的人那里得到一些关于这个问题的见解。
【问题讨论】:
标签: debugging functional-programming lazy-evaluation
我想知道如何在惰性函数式语言中实现调试。
你能使用断点、打印语句和传统技术吗?这是一个好主意吗?
据我了解,纯函数式编程不允许副作用,除了 monad。
执行顺序也无法保证。
您是否必须为要测试的每一段代码编写一个 monad?
我想从这个领域更有经验的人那里得到一些关于这个问题的见解。
【问题讨论】:
标签: debugging functional-programming lazy-evaluation
没有什么可以阻止您在延迟评估的函数式程序中使用断点。与急切求值的区别在于何时程序将在断点处停止以及跟踪的外观。当设置断点的表达式实际上正在减少时(显然),程序将停止。
而不是你习惯的堆栈跟踪,你得到的减少导致了带有断点的表达式的减少。
愚蠢的小例子。你有这个 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,即使它没有明确这样做。从中可以看出,times_two (-2) 中对 2 * x 的评估确实强制从 foo 行评估 (add_two 42) (-1)。从那里您可以像在命令式调试器中一样执行一个步骤(执行下一个缩减)。
在 Eager 语言中进行调试的另一个区别是变量可能尚未评估 thunk。例如,在上述跟踪的第 -2 步并检查 x,您会发现它仍然是一个未评估的 thunk(在 GHCi 中用括号表示)。
有关更详细的信息和示例(如何逐步执行跟踪、检查值等),请参阅 GHC 手册中的 the GHCi Debugger section。还有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 属性实际上做出了简洁明了的函数规范(命令式语言中的许多测试代码对我来说就像是另一块代码)。
避免大量调试的一个技巧是将函数分解为许多较小的子函数并尽可能多地测试它们。这在来自命令式编程时可能有点不寻常,但无论您使用哪种语言,这都是一个好习惯。
再说一次,调试!= 测试,如果某处出现问题,断点和跟踪可能会帮助您。
【讨论】:
我不认为这个话题可以在很短的时间内处理。请阅读以下链接中的论文:
【讨论】:
我从来没有深入研究过 Haskell 中的任何非常复杂的东西,但是副作用几乎消失的事实已经消除了大部分调试需求。纯函数在没有调试器的情况下测试和验证非常简单。
另一方面,我确实经历过几次我需要在 monad 中调试某些东西,在这种情况下,我已经能够打印/记录/任何东西了。
至少对于较小的程序或系统来说,调试有点过时了。强类型和静态类型检查确实进一步消除了您在过程编程中发现的传统错误。大多数错误(如果有的话)都是逻辑错误(称为错误函数、数学错误等)——非常容易交互测试。
【讨论】:
来自Clojure 的经验(它是懒惰的、实用的,并且鼓励但不强制执行纯洁性):
您可以像使用任何其他语言一样设置断点。但是,由于惰性计算,这些可能不会立即被调用,但会在强制计算惰性结构时立即被调用。
在允许副作用的惰性函数式语言(包括 Clojure)中,您可以相对轻松地插入 println 和其他调试日志记录。我个人觉得这些非常有用。你必须小心什么时候因为懒惰而被调用,但如果你根本看不到输出,这可能暗示你的代码因为懒惰而没有被评估.....
上面说了这么多,到目前为止,我从来没有需要求助于调试器。通常一些简单的测试(可能在 REPL 上)就足以验证功能代码是否正常工作,如果这些测试失败,那么通常很明显出了什么问题。
【讨论】:
请允许我宣传我自己的工具来调试懒惰问题。它帮助我在一小时内解决了我已经花了 2 天时间调试的与惰性相关的内存泄漏。
http://www.haskell.org/pipermail/haskell-cafe/2012-January/098847.html
【讨论】: