我试图理解延续是如何工作的,我有这个例子,我在Tomas Petricek和Jon Skeet的书中看到了真实世界功能编程。但这确实让我头疼,所以我必须要求一些详细的帮助。
type IntTree =
| Leaf of int
| Node of IntTree * IntTree
let rec sumTreeCont tree cont =
match tree with
| Leaf(n) -> cont(n)
| Node(left, right) ->
sumTreeCont left (fun leftSum ->
sumTreeCont right (fun rightSum ->
cont(leftSum + rightSum)))
好的,这就是我能够弄清楚自己......在第二个分支中,我们首先处理节点的左侧并传递一个lambda。这个lambda将使用两个字段right: IntTree
和cont: (int -> 'a)
来实例化一个闭包类,它将由基本案例调用。但是,似乎“内在的lambda”捕获leftSum
,但我不太明白这一切是如何组合在一起的,我不得不承认,当我试图解决这个问题时,我有点头晕。
答案 0 :(得分:9)
我认为克里斯蒂安的答案很好 - 延续传递风格实际上只是一个(不那么)简单的机械转换,你在原始源代码上做。当您逐步执行此操作时,这可能更容易看到:
1)从原始代码开始(这里,我将代码更改为每行只执行一次操作):
let rec sumTree tree =
match tree with
| Leaf(n) -> n
| Node(left, right) ->
let leftSum = sumTree left
let rightSum = sumTree right
leftSum + rightSum
2)添加continuation参数并调用它而不是返回结果(这仍然不是尾递归)。为了进行这种类型检查,我在两个子调用中添加了延续fun x -> x
,以便它们只返回总和作为结果:
let rec sumTree tree cont =
match tree with
| Leaf(n) -> cont n
| Node(left, right) ->
let leftSum = sumTree left (fun x -> x)
let rightSum = sumTree right (fun x -> x)
cont (leftSum + rightSum)
3)现在,让我们更改第一个递归调用以使用延续传递样式 - 将身体的其余部分提升到延续:
let rec sumTree tree cont =
match tree with
| Leaf(n) -> cont n
| Node(left, right) ->
sumTree left (fun leftSum ->
let rightSum = sumTree right (fun x -> x)
cont (leftSum + rightSum) )
4)并为第二次递归调用重复相同的事情:
let rec sumTree tree cont =
match tree with
| Leaf(n) -> cont n
| Node(left, right) ->
sumTree left (fun leftSum ->
sumTree right (fun rightSum ->
cont (leftSum + rightSum) ))
答案 1 :(得分:7)
如果您首先考虑使用此表达式计算树的总和,那么可能更容易理解:
let rec sumTree tree =
match tree with
| Leaf(n) -> n
| Node(left, right) ->
sumTree left + sumTree right
这个解决方案的问题在于,由于过多的堆栈帧分配,它会溢出大型树的堆栈。解决方案是使用确保递归调用处于尾部位置意味着您不能在调用之后执行任何操作(在上面的情况下,在递归调用之后执行添加)。在这种情况下,编译器可以消除不必要的堆栈帧,从而避免溢出。解决这个问题的技巧是使用连续传递样式,如Tomas'和Jon的解决方案。如您所见,此处使用的延续确保在递归调用之后不执行任何操作。
答案 2 :(得分:4)