[文档翻译] Haskell Debug

GHCi 包含一个基本的 Debug 工具。利用此工具可以暂停一个运行的程序从而可以进行变量的检查。在 GHCi 中 Debugger 是默认开启的,不需要指定额外的标志。唯一的限制就是只能在解释运行时才能使用断点(breakpoints)和单步(single-stepping)。

GHCi Debugger 提供一下功能:

  1. 对函数和表达式设置断点。
  2. 支持单步运行。
  3. 可以在 tracing mode 运行程序
  4. Exeception 可以被当做断点。

目前不支持获取 “stack trace”,但是可以使用 tracing 和 history 特性。

1. Breakpoints and Inspecting variables

首先用快速排序作为一个例子,代码如下:

1
2
3
4
5
1| qsort [] = []
2| qsort (a:as) = qsort left ++ [a] ++ qsort right
3| where (left, right) = (filter (<=a) as, filter (>a) as)
4|
5| main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
  1. 加载代码至 GHCi
1
2
3
4
5
6
user@hostname:~$ Temp ghci
GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help
Prelude> :l qsort.hs
[1 of 1] Compiling Main ( qsort.hs, interpreted )
Ok, modules loaded: Main.
*Main>
  1. 在 qsort.hs 第二行设置一个断点
1
2
3
*Main> :break 2
Breakpoint 0 activated at qsort.hs:2:16-47
*Main>

:break 2 命令在最近加载文件的第 2 行设置了一个断点,这里就是 qsort.hs 文件。

  1. 运行程序
1
2
3
4
5
6
7
*Main> main
Stopped at qsort.hs:2:16-47
_result :: [Integer] = _
a :: Integer = 8
left :: [Integer] = _
right :: [Integer] = _
[qsort.hs:2:16-47] *Main>

程序将在断点处停止,提示符的改变也意味着我们当前停止在 qsort.hs:2:16-47 断点处。使用 :list 命令可以更清楚的显示当前断点位置。

1
2
3
4
5
6
[qsort.hs:2:16-47] *Main> :list
1 qsort [] = []
2 qsort (a:as) = qsort left ++ [a] ++ qsort right
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 where (left, right) = (filter (<=a) as, filter (>a) as)
[qsort.hs:2:16-47] *Main>

:list 命令将显示断点附近的代码,如果显示设备支持,断点处将会特别标注。

GHCi 会提供对断点处表达式的变量绑定(a,left,right),并且还有一个对于表达式结果的绑定(_result)。这些变量和通常在 GHCi 中定义的变量一样,可以在提示符中使用它们,用 :type 命令来获取它们的类型。用 :print 或 :force 来打印它们。:force 相比于 :print 命令会将表达式计算至常态并显示。

1
2
3
4
[qsort.hs:2:16-47] *Main> :print right
right = (_t1::[Integer])
[qsort.hs:2:16-47] *Main> :force right
right = [23,11,18]

同样,使用 seq 可以计算出单个的值,而不是像 :force 一样计算出整个表达式。

1
2
3
4
5
6
[qsort.hs:2:16-47] *Main> :print left
left = (_t1::[Integer])
[qsort.hs:2:16-47] *Main> seq _t1 ()
()
[qsort.hs:2:16-47] *Main> :print left
left = 4 : (_t2::[Integer])

最后,我们可以用 :continue 命令让程序继续运行,直到遇到下一个断点。

1
2
3
4
5
6
7
[qsort.hs:2:16-47] *Main> :continue
Stopped at qsort.hs:2:16-47
_result :: [Integer] = _
a :: Integer = 4
left :: [Integer] = _
right :: [Integer] = _
[qsort.hs:2:16-47] *Main>

1.1 Setting breakpoints

设置断点的方式有很多中,其中最方便的应该是使用最顶层的函数名

1
:break identifier

identifier 是任何当前加载进入 GHCi 的顶层函数名。

断点还可以按行(和列)来设置:

1
2
3
4
:break line
:break line column
:break module line
:break module line column

1.2 Listing and deleting breakpoints

使用 :show breaks 可以显示当前断点。

1
2
3
4
5
[qsort.hs:2:16-47] *Main> :break main
Breakpoint 1 activated at qsort.hs:5:8-48
[qsort.hs:2:16-47] *Main> :show breaks
[0] Main qsort.hs:2:16-47
[1] Main qsort.hs:5:8-48

删除断点可以使用 :delete 命令并指定断点编号。

1
2
3
4
[qsort.hs:2:16-47] *Main> :delete 1
[qsort.hs:2:16-47] *Main> :show breaks
[0] Main qsort.hs:2:16-47
[qsort.hs:2:16-47] *Main>

2. Single-stepping

单步是观测程序运行的最好的方法,并且也是寻找 Bug 的有利工具。使用 :step 命令,将启用程序中的全部断点并运行程序。使用 :steplocal 将只启用当前顶层函数中的所有断点。同样,使用 :stepmodule 将只在当前模块中进行单步。

1
2
3
4
5
6
7
8
9
*Main> :step main
Stopped at qsort.hs:5:8-48
_result :: IO () = _
[qsort.hs:5:8-48] *Main> :list
4
5 main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6
[qsort.hs:5:8-48] *Main>

在单步中使用 :list 命令,可以很方便的获取当前位置。另外,在 GHCi 中可以设置当遇见断点时自动执行命令。因此我们可以让它自动执行 :list 命令。

1
2
3
4
5
6
7
8
9
[qsort.hs:5:8-48] *Main> :set stop :list
[qsort.hs:5:8-48] *Main> :step
Stopped at qsort.hs:5:15-47
_result :: [Integer] = _
4
5 main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6
[qsort.hs:5:15-47] *Main>

3. Nested breakpoints

当 GHCi 处于一个断点处时,如果此时在命令行键入一个命令并遇到一个新的断点,那么新的断点将会作为当前断点,旧的断点将会保存在栈中。示例如下:

1
2
3
4
5
6
7
8
9
*Main> :break qsort
Breakpoint 0 activated at qsort.hs:(1,1)-(3,59)
*Main> main
Stopped at qsort.hs:(1,1)-(3,59)
_result :: [t] = _
[qsort.hs:(1,1)-(3,59)] *Main> :step qsort [1,3,6,5]
Stopped at qsort.hs:(1,1)-(3,59)
_result :: [t] = _
... [qsort.hs:(1,1)-(3,59)] *Main>

当第一次停止在 qsort 上时,我们再次执行 :step [1,3,6,5],这个新的命令将又会停止在第一步。此时可以观察到提示符的变化。提示符前有 “….” 前缀,表示当前断点处还有一个保存的断点。使用 :show context 命令可以观察保存的断点处。

1
2
3
4
5
6
... [qsort.hs:(1,1)-(3,59)] *Main> :show context
--> main
Stopped at qsort.hs:(1,1)-(3,59)
--> qsort [1,3,6,5]
Stopped at qsort.hs:(1,1)-(3,59)
... [qsort.hs:(1,1)-(3,59)] *Main>

删除当前断点处可以使用 :abandon 命令。

1
2
3
... [qsort.hs:(1,1)-(3,59)] *Main> :abandon
[qsort.hs:(1,1)-(3,59)] *Main> :abandon
*Main>

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 命令来运行表达式。