Erlang 在出现问题时会产生很好的堆栈跟踪,当程序员想要找出出错的原因时,这很有帮助。然而,在存在高阶函数的情况下,生成堆栈跟踪的机制似乎不足。例如,比较下面的两个示例(我在代码的注释中添加了生成的堆栈跟踪)。
我之前读过关于为惰性评估(例如在 Haskell 中)生成堆栈跟踪的困难。但是由于 Erlang 的评估是严格的,我本来期望这里有更好的结果。
我的问题是:是什么让高阶函数成为 Erlang 用来生成堆栈跟踪的机制的问题?是否有其他已知的技术可以产生更好的结果?
1 -module(test).
2 -export([f/1,a/1]).
3
4 % Auxilary function to print stack trace (by throwing an exception).
5
6 s(X) when X < 0 -> 0.
7
8 % Example 1: Stack trace in presence of a higher order function.
9 %
10 % > test:f(1).
11 % ** exception error: no function clause matching test:s(1) (test.erl, line 6)
12 % in function test:g/2 (test.erl, line 15)
13
14 f(X) -> h(fun g/2,X).
15 g(X,Y) -> s(X) - Y.
16 h(I,X) -> I(X,3).
17
18 % Example 2: Stack trace for chain of 1st order function applications.
19 %
20 % > test:a(1).
21 % ** exception error: no function clause matching test:s(1) (test.erl, line 6)
22 % in function test:c/1 (test.erl, line 28)
23 % in call from test:b/1 (test.erl, line 27)
24 % in call from test:a/1 (test.erl, line 26)
25
26 a(X) -> b(X) + 1.
27 b(X) -> c(X) + 2.
28 c(X) -> s(X) + 3.
这不是使用高阶函数本身的结果,而是尾部调用优化的结果。 如果一个函数在返回之前做的最后一件事是调用另一个函数,那么 Erlang VM 会优化堆栈帧,因为它不再需要了。 (这就是为什么您可以在不溢出堆栈的情况下执行递归的原因。
在您的示例中,f
和 h
都执行尾部调用,因此它们的堆栈帧在堆栈跟踪中不可见。 另一方面,a
、 b
和 c
不进行尾部调用,因为它们必须在返回调用方之前对它们正在调用的函数的结果执行加法。