Debugging 如何在惰性函数式编程语言中实现调试?

Debugging 如何在惰性函数式编程语言中实现调试?,debugging,functional-programming,lazy-evaluation,Debugging,Functional Programming,Lazy Evaluation,我想知道如何在惰性函数语言中实现调试。 您可以使用断点、打印语句和传统技术吗?这是个好主意吗? 我的理解是,除了单子之外,纯函数式编程不允许有副作用。 执行顺序也不保证。 您是否需要为要测试的每一段代码编写monad? 我想从这个领域更有经验的人那里了解一下这个问题 我认为这个话题不可能在短时间内解决。请阅读以下链接提供的论文: 在Haskell中,我从来没有深入研究过任何非常复杂的事情,但是副作用几乎消失的事实已经消除了调试的大部分需要。纯函数在没有调试器的情况下非常易于测试和验证 另一

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

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


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

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


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

    没有任何东西可以阻止您在延迟评估的函数程序中使用断点。与急切求值的区别在于程序何时在断点处停止以及跟踪的外观。当设置断点的表达式实际减少时(显然),程序将停止

    您不需要使用堆栈跟踪,而是获取导致表达式上带有断点的缩减的缩减

    愚蠢的小例子。你有这个Haskell计划

    add_two x = 2 + x
    
    times_two x = 2 * x
    
    foo = times_two (add_two 42)
    
    在第一行(
    add_two
    )上放置一个断点,然后计算
    foo
    。当程序在断点上停止时,用一种急切的语言,您可能希望有一个类似

    add_two
    foo
    
    而且
    times\u 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>
    
    如果要添加跟踪语句,打印将在IO monad中返回一个操作,因此您必须调整要放入该跟踪语句的函数的签名,以及调用该跟踪语句的函数的签名

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

    trace :: String -> a -> a
    
    它输出字符串并返回第二个参数。以纯函数的形式编写是不可能的(如果您真的想输出字符串,也就是说)。它在发动机罩下使用
    unsafePerformIO
    。您可以将其放入纯函数中以输出跟踪打印

    您是否需要为要测试的每一段代码编写monad

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

    命令式调试技术是否是一个好主意取决于具体情况(通常)。我发现使用QuickCheck/SmallCheck进行测试比命令式语言中的单元测试更有用,所以我会首先使用这一方法,以尽可能避免调试。QuickCheck属性实际上是非常简洁的函数规范(命令式语言中的许多测试代码在我看来只是另一堆代码)

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

    再说一遍,调试!=测试以及如果某个地方出了问题,断点和跟踪可能会帮助您解决问题。

    根据经验(这是懒惰的、功能性的,鼓励但不强制执行纯度):

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

    • 在允许副作用的惰性函数语言中(包括Clojure),您可以相对轻松地插入println和其他调试日志。我个人觉得这些很有用。您必须小心这些代码何时因懒惰而被调用,但如果您根本看不到输出,则可能提示您的代码因懒惰而未被评估


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

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


    感谢您的精彩回答并深入了解Haskellhi,最后两个链接已失效!
    trace :: String -> a -> a