问题是,我们既希望做尾递归调用,还要在尾递归调用完成后,再执行一些代码。这看起来是一个棘手的问题,但有一个有趣的解决方案。我们把要在递归调用完成后执行的所有代码,拿来作为参数值,提供给递归调用,这样,我们要写的函数将仅只包含一个递归调用。
可以把这个看作是另一种类型的累加器参数:我们不是累加值,而是累加“需要在以后运行的代码”。现在的问题是,我们如何能拿到其余的代码,并把它作为给函数的参数值?这可能要感谢一等函数(first class functions),这个最后的参数就称为连续(continuation),因为它指定运行应该如何继续。
在看过一些实际的例子后,这一切会变得更清楚。清单 10.17 是一个简单的函数,首先,以通常的风格实现,然后,使用连续。这里,我们将使用 C#,只有一个新的概念要理解,但请记住,C# 不支持尾递归:在 C# 中,这个技术不能作为递归的优化而使用。(在 C# 中连续仍然可用,只是不是为递归的。)
清单 10.17 用连续写代码(C#)
// Reports result as return value
int StringLength(string s) {
returns.Length;
}
void AddLengths() { [1]
int x1= StringLength("One");
int x2= StringLength("Two");
Console.WriteLine(x1+ x2);
}
// Reports result using continuations
void StringLengthCont(string s,Action<int> cont) {
cont(s.Length); [2]
}
void AddLengthsCont() {
StringLengthCont("One", x1 => [3]
StringLengthCont("Two", x2 => [4]
Console.WriteLine(x1 + x2)
));
}
在两个版本中,我们首先声明了计算字符串长度的函数。按通常的编程风格,把结果作为返回值;当使用连续时,我们增加了一个函数(连续)作为最后一个参数值。要返回结果,StringLengthCont 函数调用这个连续[2]。我们将使用函数代替通常的 return 语句,这样,值是作为给函数的参数值 ,而不是把它作为结果存储在栈中。
下一个函数,AddLengths,[1]计算两个字符串的长度,再把这两个值加起来,再输出结果。在使用连续的版本中,只包括一个顶层对StringLengthCont 函数的调用[3]。调用的第一个参数值是字符串,第二个参数值是连续。顶层的调用是函数要做的最后事情,因此,在 F# 中,可以使用尾调用运行,它不会占用任何栈空间。
连续接收第一个字符串的长度作为参数值;在内部,再为第二个字符串调用StringLengthCont。另外,把连续作为最后参数值给它,它只被调用一次,我们可以计算两个长度的和,并输出结果。在 F# 中,在连续内部的调用[4]还是尾调用,因为它是代码在 lambda 函数中所要做的最后工作。现在,我们要看一下使用这种编程风格,来优化我们以前的函数,计算树中元素的和。