【问题标题】:Processing a tree in F# using continuations使用延续在 F# 中处理树
【发布时间】:2015-02-27 00:55:45
【问题描述】:

我试图了解延续是如何工作的,我在 Tomas Petricek 和 Jon Skeet 的 Real World Functional Programming 一书中遇到了这个例子。但这真的让我头晕目眩,所以我必须寻求一些详细的帮助..

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,但我不太明白它们是如何组合在一起的,我不得不承认,当我试图弄清楚这一点时,我有点头晕。

【问题讨论】:

标签: lambda f# continuations


【解决方案1】:

如果你首先考虑这个表达式来计算树的总和,可能会更容易理解:

let rec sumTree tree =
   match tree with
   | Leaf(n) -> n
   | Node(left, right) -> 
       sumTree left + sumTree right

此解决方案的问题在于,由于过多的堆栈帧分配,它会溢出大树的堆栈。解决方案是使用确保递归调用位于尾部位置,这意味着您不能在调用之后执行任何操作(在上述情况下,在递归调用之后执行加法)。在这种情况下,编译器可以消除不必要的堆栈帧,从而避免溢出。解决这个问题的技术是使用 Tomas 和 Jon 的解决方案中的延续传递风格。如您所见,此处使用的延续确保在递归调用之后不执行任何操作。

【讨论】:

    【解决方案2】:

    我认为克里斯蒂安的回答是一个很好的答案 - 延续传递风格实际上只是您对原始源代码进行的(不是那么)简单的机械转换。当您逐步执行此操作时,可能会更容易看到:

    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) 添加延续参数并调用它而不是返回结果(这仍然不是尾递归)。为了进行这种类型检查,我在两个子调用中添加了延续 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) ))
    

    【讨论】:

    • 非常感谢您的澄清!到目前为止,我认为您的书非常出色,但我似乎需要一点时间才能掌握延续的概念:)。
    • 很好的答案。显然步骤2)的变换需要应用于DU的所有情况:| Leaf(n) -> cont n
    • @kaefer 谢谢 - 已修复。
    【解决方案3】:

    我在试图理解这一点的过程中制作了一张 Visio 绘图,我想我可以在这里分享它以防它帮助其他人。我意识到它可能最终会让某些人更加困惑,但对于视觉学习者(比如我)来说,我觉得它让事情变得更加清晰,可以通过一个例子来说明如何处理这样的树。

    【讨论】:

      猜你喜欢
      • 2011-10-30
      • 2019-05-31
      • 2011-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-30
      • 1970-01-01
      相关资源
      最近更新 更多