[文档翻译] Haskell Debug
GHCi 包含一个基本的 Debug 工具。利用此工具可以暂停一个运行的程序从而可以进行变量的检查。在 GHCi 中 Debugger 是默认开启的,不需要指定额外的标志。唯一的限制就是只能在解释运行时才能使用断点(breakpoints)和单步(single-stepping)。
GHCi Debugger 提供一下功能:
- 对函数和表达式设置断点。
- 支持单步运行。
- 可以在 tracing mode 运行程序
- Exeception 可以被当做断点。
目前不支持获取 “stack trace”,但是可以使用 tracing 和 history 特性。
1. Breakpoints and Inspecting variables
首先用快速排序作为一个例子,代码如下:
1 | 1| qsort [] = [] |
- 加载代码至 GHCi
1 | user@hostname:~$ Temp ghci |
- 在 qsort.hs 第二行设置一个断点
1 | *Main> :break 2 |
:break 2 命令在最近加载文件的第 2 行设置了一个断点,这里就是 qsort.hs 文件。
- 运行程序
1 | *Main> main |
程序将在断点处停止,提示符的改变也意味着我们当前停止在 qsort.hs:2:16-47 断点处。使用 :list 命令可以更清楚的显示当前断点位置。
1 | [qsort.hs:2:16-47] *Main> :list |
:list 命令将显示断点附近的代码,如果显示设备支持,断点处将会特别标注。
GHCi 会提供对断点处表达式的变量绑定(a,left,right),并且还有一个对于表达式结果的绑定(_result)。这些变量和通常在 GHCi 中定义的变量一样,可以在提示符中使用它们,用 :type 命令来获取它们的类型。用 :print 或 :force 来打印它们。:force 相比于 :print 命令会将表达式计算至常态并显示。
1 | [qsort.hs:2:16-47] *Main> :print right |
同样,使用 seq 可以计算出单个的值,而不是像 :force 一样计算出整个表达式。
1 | [qsort.hs:2:16-47] *Main> :print left |
最后,我们可以用 :continue 命令让程序继续运行,直到遇到下一个断点。
1 | [qsort.hs:2:16-47] *Main> :continue |
1.1 Setting breakpoints
设置断点的方式有很多中,其中最方便的应该是使用最顶层的函数名
1 | :break identifier |
identifier 是任何当前加载进入 GHCi 的顶层函数名。
断点还可以按行(和列)来设置:
1 | :break line |
1.2 Listing and deleting breakpoints
使用 :show breaks 可以显示当前断点。
1 | [qsort.hs:2:16-47] *Main> :break main |
删除断点可以使用 :delete 命令并指定断点编号。
1 | [qsort.hs:2:16-47] *Main> :delete 1 |
2. Single-stepping
单步是观测程序运行的最好的方法,并且也是寻找 Bug 的有利工具。使用 :step 命令,将启用程序中的全部断点并运行程序。使用 :steplocal 将只启用当前顶层函数中的所有断点。同样,使用 :stepmodule 将只在当前模块中进行单步。
1 | *Main> :step main |
在单步中使用 :list 命令,可以很方便的获取当前位置。另外,在 GHCi 中可以设置当遇见断点时自动执行命令。因此我们可以让它自动执行 :list 命令。
1 | [qsort.hs:5:8-48] *Main> :set stop :list |
3. Nested breakpoints
当 GHCi 处于一个断点处时,如果此时在命令行键入一个命令并遇到一个新的断点,那么新的断点将会作为当前断点,旧的断点将会保存在栈中。示例如下:
1 | *Main> :break qsort |
当第一次停止在 qsort 上时,我们再次执行 :step [1,3,6,5],这个新的命令将又会停止在第一步。此时可以观察到提示符的变化。提示符前有 “….” 前缀,表示当前断点处还有一个保存的断点。使用 :show context 命令可以观察保存的断点处。
1 | ... [qsort.hs:(1,1)-(3,59)] *Main> :show context |
删除当前断点处可以使用 :abandon 命令。
1 | ... [qsort.hs:(1,1)-(3,59)] *Main> :abandon |
4. The _result variable
当程序停止在断点处或进行单步时,GHCi 绑定了当前表达式的值到 _result 变量。由于暂停了当前表达式的计算,因此 _result 是不可用的,但是可以使用 :force 来进行强制计算。如果 _result 的类型是已知并可显示的,那么在命令行键入 _result 就可以显示它。但是这样做有一个警告,计算 _result 很有可能会出发将来的断点,因此在计算 _result 时可能需要键入 :continue 命令。除非使用 :force 命令,它将忽略将来的断点。
5. Tracing and history
通常在 Debug 时需要确定的问题就是"程序是如何运行到这里的?"。传统的 Debugger 工具会提供必要的 stack-tracing 特性,可以显示当前程序的调用堆栈。但是不幸运的是,因为 Haskell 是按需计算的,所以难以提供这样的特性。Haskell 中的 Stack 与传统的调用堆栈几乎没有相似之处。理想情况下,除了动态调用堆栈之外,GHCi 还应该维护一个单独的词汇调用堆栈,事实上,这正是分析系统(Profiling)所做的,以及其他 Haskell 调试器所做的。然而,目前 GHCi 还没有维护词汇调用堆栈(需要克服一些技术挑战)。相反,我们提供了一种从断点回溯到之前表达式的方法:本质上,这就像单步后退,在许多情况下,应该提供足够的信息来回答"程序是如何运行到这里的?"的问题。
使用 Tracing,可以利用 :tracing 命令来运行表达式。