如何更好地调试Haskell代码?

6

这里曾经提出过一个类似的问题,但那已经是十年前的事情了,我希望在此期间有一些进展,现在可能会得到不同的答案。

目前我正在使用VSCode,并安装了“Haskell GHCi Debug Adapter Phoityne”。它可以工作,但由于Haskell的惰性(纯代码和IO代码的分离以及Haskell的函数式特性),调试代码仍然很困难。

一些解决这些问题的方案似乎已被提出。例如,我找到了this article,它比较了Haskell中的调试技术,并命名了几个尝试通过引入函数“观察”来改善体验的包。其中,HoodHoedHat都被命名,我还在hackage上找到了debug
然而,不幸的是,我无法让它们正常工作。安装Hood时,我收到错误消息,指出它有一个依赖项FPretty与较新版本的基础包不兼容(自2018年以来已经提出了问题)。同样的问题也出现在Hoed上。当我安装Hat时,出现了许多错误,开始抱怨缺少模块,并在解释程序时出现调试崩溃。

因此,我无法测试任何一个。然而,我真正想要的是一个图形化调试器(最好是VSCode中已实现版本的改进版!),它能跳转到正在检查的源代码行,然后显示一个小窗口,逐步展开递归(可以不进行评估,甚至更好的是,在递归完成之后提供逐步评估选项,即在实际评估之前),类似于这篇维基文章中foldr的效果。

以上的软件包中是否有一个可以实现这个功能?对于Haskell社区来说,拥有这样的VSCode功能来改善生态系统并吸引新手会很好吧?或者这已经是可能的了,只是我不知道如何正确配置Phoityne?或者您有完全不同的方法来高效地调试Haskell?您的工作流程是什么?


1
虽然我同意这对新手来说可能非常有帮助,但在调试实际规模的项目时几乎没有效果。在我看来,最好的调试方法是不要调试,而是进行重构和单元测试,以至于永远不需要逐步执行任何代码。Haskell的强类型在这方面非常有帮助。 - leftaroundabout
通常不是说,由于更大(“现实世界”)的问题而导致代码变得更加复杂时,调试才变得必要吗?我认为这就是调试器最初被发明的原因。调试不仅有助于查找代码中的错误,还可以观察值如何变化,而无需手动打印所有内容(在Haskell中,由于IO单子,这将特别麻烦)。 - exchange
调试出现的原因有很多。其中一些在Haskell中完全可以避免(例如,值永远不会改变,因此也没有必要观察它们如何不改变)。复杂性既增加了错误的可能性,也增加了处理错误的难度 - 但是最好的解决方案不是通过武器竞赛使调试器更加复杂,而是将代码重构为较少复杂组件,这些组件可以轻松进行测试和调试而无需特殊工具。(但是,“手动打印”偶尔仍然有用,不在IO单子中,而是使用Debug.Trace。) - leftaroundabout
1
好的,真正酷的工具是显示已经评估过的数据结构,并通过它们的源代码表示thunk,然后可以根据需要进入。但那与我们通常考虑的调试器非常不同。命令式的步骤概念并不适用于Haskell——因为评估顺序通常是无序的,因为语言本身(除了seq)没有定义。 - leftaroundabout
1
没有调试器那么方便和人性化,我同意拥有一个调试器会很棒,但是为了追踪我们自己定义的递归函数的执行,一种解决方案是以“开放递归”风格编写它,并手动添加一些工具:https://dev59.com/isDqa4cB1Zd3GeqPnubd#67787958 - danidiaz
显示剩余3条评论
1个回答

2
我从未成功使用现有的调试解决方案,所以我只使用自定义 ghci 命令和 :set stop :status 在每个断点停止时显示变量和其他有用信息。
在 ghci 中: :br someFunction - 在 someFunction 处设置断点 :br SomeModule.someFunction - 在模块中的函数处设置断点 :br SomeModule 150 - 在特定行处设置断点 :stl - 步进本地 (跳过) :st - 步进 (跳入) :status - 显示状态 ~/.ghci 的内容:
-- Fancy debugging
_nl = "putStrLn \"\"\n"
_history = "putStrLn \"HISTORY:\"\n :history \n"
_context = "putStrLn \"CONTEXT:\"\n :sh context\n"
_breaks = "putStrLn \"BREAKS:\"\n :sh breaks\n"
_bindings = "putStrLn \"BINDINGS:\"\n :sh bindings\n"
_list = "putStrLn \"LIST:\"\n :list\n"
_status = \_ -> return $ ":!clear\n " ++ _breaks ++ _nl ++ _history ++ _nl ++ _bindings ++ _nl ++ _context ++ _nl ++ _list ++ _nl

-- Useful commands
:def hoogle \x -> return $ ":!hoogle \"" ++ x ++ "\""
:def hinfo \x -> return $ ":!hoogle --info \"" ++ x ++ "\""
:def stl \x -> return $ ":steplocal " ++ x
:def status _status
:def fprint \x -> return $ ":force " ++ x ++ "\n" ++ "print " ++ x
:def fpprint \x -> return $ ":force " ++ x ++ "\n" ++ "pPrint " ++ x

-- Show status on breakpoint
:set stop :status

除此之外,还有haskell-language-server和ghcid可用于更快的错误和类型检查。

非常感谢您的回答。您能否提供有关如何使用 .haskline 文件的更多信息?当文件位于根目录中时,ghci 是否能够访问其中定义的命令,或者是否需要加载它或安装一个自动加载它的包? - exchange
1
@exchange 对不起,我说错了 ~/.haskeline,实际上是 ~/.ghci(ghci 配置文件)。Haskeline 只是用于 ghci 历史记录和其他次要设置。 https://mpickering.github.io/ghc-docs/build-html/users_guide/ghci.html#the-ghci-and-haskeline-files - Vito Canadi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接