使用continuation在F#中处理树

时间:2014-12-30 21:07:52

标签: lambda f# continuations

我试图理解延续是如何工作的,我有这个例子,我在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: IntTreecont: (int -> 'a)来实例化一个闭包类,它将由基本案例调用。但是,似乎“内在的lambda”捕获leftSum,但我不太明白这一切是如何组合在一起的,我不得不承认,当我试图解决这个问题时,我有点头晕。

3 个答案:

答案 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)

我在尝试理解这个过程中制作了一个Visio绘图,我想我可以在这里分享它,以防它帮助其他人。我意识到它可能最终会让一些人感到困惑,但是对于视觉学习者(像我一样),我觉得它让事情变得更加清晰,从中可以看出像这样处理树的样子。

Processing tree with continuations an illustrative example