10.3.2 写代码使用连续
问题是,我们希望做尾递归调用,但是,在尾递归调用完成后,仍然有一些代码要执行。这看起来像一个棘手的问题,但有一个有趣的解决方案。我们把要在递归调用完成后执行的所有代码,拿来作为一个参数值,提供给递归调用。这意味着,我们要写的函数将仅包含一个单独的递归调用。
把此看作是另一种累加器参数,而不是累加值,我们将累加“更多的代码在以后运行”。现在,我们怎么能取得其余的代码,并把它作为一个函数的参数值?由于有了一级函数,这是可能的。最后一个参数称为连续,因为,它指定运行应该如何继续。
在看过一些实际的例子后,这一切会变得更清楚。清单 10.17 显示了一个简单的函数,首先,以通常的风格实现,然后,使用连续。这里,我们使用 C#,只有一个新的概念要理解,但请记住,C# 不支持尾递归,这个技术不能在 C# 中作为递归的优化而使用。(在 C# 中连续仍然是有用的,只是没有递归。)
Listing 10.17 Writing code using continuations (C#)
// Reports result as return value
int StringLength(string s) {
return s.Length;
}
void AddLengths() {
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);
}
void AddLengthsCont() {
StringLengthCont("One", x1 =>
StringLengthCont("Two", x2 =>
Console.WriteLine(x1 + x2)
));
}
在这两个版本中,我们首先声明一个函数计算字符串的长度。在通常的编程风格中,把结果作为返回值。当使用连续时,我们添加了一个函数(连续)作为最后一个参数值。要返回结果,StringLengthCont 函数调用这个连续。我们将使用函数,代替通常的 return 语句,这意味着,这个值是作为一个函数的参数值 ,而不是把它作为结果存储在堆栈中。
下面的函数,AddLengths,计算两个字符串的长度,把这些值加起来,并打印出结果。在使用连的版本中,它只包括一个单一的顶层调用 StringLengthCont 函数。调用的第一个参数值是一个字符串,第二个是一个连续。顶层的调用是函数做的最后的事情,因此,在 F# 中,可以使用尾调用运行,它不会占用任何堆栈空间。
连续接收第一个字符串的长度作为参数值。在它里面,我们对第二个字符串调用 StringLengthCont。另外,把连续作为最后参数值给它,它只被调用一次,我们可以计算这两个长度的和,并打印出结果。在 F# 中,在连续内部的调用还可以尾调用,因为,它是代码在 lambda 函数中做的最后的事情。现在,让我们看一下如何能够使用这种编程风格,以优化我们以前的函数,计算树中的元素和。
使用连续处理树
要把我们以前实现的 sumTree 函数,转变成一个使用连续的版本,首先,要给这个函数添加一个额外的参数(连续)。还需要更新函数返回结果的方式。不要简单地返回值,将调用作为参数值给定的延续。 清单 10.18 所示的代码是最终版本。
Listing 10.18 Sum elements of a tree using continuations (F# Interactive)
> let rec sumTreeCont tree cont =
match tree with
| Leaf(num) -> cont(num)
| Node(left, right) �C>
sumTreeCont left (fun leftSum ->
sumTreeCont right (fun rightSum �C>
cont(leftSum + rightSum)));;
val sumTreeCont : IntTree -> (int -> 'a) �C> 'a
修改叶子的分支情况很容易,因为,它以前就从叶子返回值。第二个情况有趣得多,我们使用的模式相似于在以前的 C# 示例。我们调用该函数计算左子树元素的的和(这是一个尾递归),并把一个 lambda 函数给它作为第二个参数值。在 lambda 函数里面,对右子树做类似的事情(也是一个尾递归调用)。一旦我们有两个子树的和,调用最初作为参数得到的连续,(这还是一个尾递归调用)。
对于我们刚刚写的这个函数,还有一个有趣的事情,是它的类型签名。通常,我们不显式写出任何类型,F# 会为我们推断类型。该函数取树作为第一个参数,连续作为第二参数。现在,这个连续有一个 int �C> 'a 类型,函数的整体结果是 'a。换句话说,整个函数的返回类型与连续返回的类型相同。
前面我们提到,在代码中所有的递归调用现在都是尾递归,所以,我们可以在不平衡树上尝试这个函数,它在以前的版本中是失败的:
> sumTreeCont imbalancedTree (fun r �C>
printfn "Result is: %d" r);;
Result is: 8736
val it : unit = ()
> sumTreeCont imbalancedTree (fun a -> a);;
val it : int = 8736
正如你可以看到的,现在的代码可以运行非常大的树,而没有任何麻烦。在第一个例子中,我们直接在连续中打印出结果,而连续不返回任何值,所以,表达式的整体结果是?? unit。在第二种情况下,我们给它一个恒等函数(只返回参数值的函数)作为连续。恒等函数在 F# 库中已经有了,所以,我们可以写 id。 连续返回类型是 int,从调用 sumTreeCont 返回的值是树中所有元素的和。