使用连续传递样式简化多路树遍历

问题描述

我着迷于 the approach used in this blog post 使用 cps 遍历玫瑰树,又名多路树,又名 n 叉树。

这是我的代码删除了类型注释并更改了名称,这是我在尝试理解该技术时所做的:

type 'a Tree = Node of 'a * 'a Tree list | Leaf of 'a

let rec reduce recCalls cont =
    match recCalls with
    | [] -> [] |> cont 
    | findMaxCall :: pendingCalls ->
        findMaxCall (fun maxAtNode ->
                    reduce pendingCalls (fun maxVals -> maxAtNode :: maxVals |> cont))
        
let findMaxOf (roseTree : int Tree) =
    let rec findMax tr cont =
        match tr with
        | Leaf i -> i |> cont
        | Node (i,chld) ->
            let recCalls = chld |> List.map findMax 
            reduce recCalls (fun maxVals -> List.max (i :: maxVals) |> cont)
    findMax roseTree id 
    
// test it
let FindMaxOfRoseTree =
    let t = Node (1,[ Leaf 2; Leaf 3 ])
    let maxOf = findMaxOf t //will be 3
    maxOf

我的问题是,我发现这种方法很难遵循。相互递归(假设这是正确的术语)对我的傻瓜大脑来说真的很聪明,但我在试图理解它的工作原理时迷失了方向,即使在使用简单的例子和​​手动写下步​​骤等时也是如此。

我需要对玫瑰树使用 cps,我将进行需要 cps 的那种遍历,因为就像这个例子一样,基于我的树节点的计算结果要求节点的子节点是首先计算。无论如何,我确实喜欢 cps,我想提高我对它的理解。

所以我的问题是:有没有其他方法可以在玫瑰树上实施 cps,我可以更好地理解?有没有办法重构上面的代码,让它更容易理解(消除相互递归?)

如果有上述方法名称,或者我可以阅读一些资源/书籍以更好地理解它,也欢迎提供提示

解决方法

CPS 肯定会令人困惑,但您可以采取一些措施来简化此代码:

  • 从您的类型中删除 Leaf 大小写,因为它是多余的。叶子只是一个带有空子列表的 Node
  • 将通用 CPS 逻辑与特定于玫瑰树的逻辑分开。
  • 使用 continuation monad 来简化 CPS 代码。

首先,让我们定义 continuation monad

type ContinuationMonad() =
    member __.Bind(m,f) = fun c -> m (fun a -> f a c)
    member __.Return(x) = fun k -> k x

let cont = ContinuationMonad()

使用此构建器,我们可以定义通用 CPS reduce 函数,该函数将“不完整”计算列表组合为单个不完整计算(其中不完整计算是任何采用 't -> 'u 类型的延续的函数) {1}} 并使用它生成 'u 类型的值)。

let rec reduce fs =
    cont {
        match fs with
        | [] -> return []
        | head :: tail ->
            let! result = head
            let! results = reduce tail
            return result :: results
    }

我认为这当然更清楚,但它可能看起来像魔术。理解此构建器的 let! x = f 的关键是 x 是传递给 f 的隐含延续的值。这使我们能够摆脱大量的 lambda 表达式和嵌套括号。

现在我们准备好处理玫瑰树了。这是简化的类型定义:

type 'a Tree = Node of 'a * 'a Tree list

let leaf a = Node (a,[])

在树中寻找最大值现在看起来像这样:

let rec findMax (Node (i,chld)) =
    cont {
        let! maxVals = chld |> List.map findMax |> reduce
        return List.max (i :: maxVals)
    }

请注意,这里没有相互递归。 reducefindMax 都是自递归的,但 reduce 不调用 findMax 并且对玫瑰树一无所知。

您可以像这样测试重构的代码:

let t = Node (1,[ leaf 2; leaf 3 ])
findMax t (printfn "%A")   // will be 3

为了方便起见,我创建了 a gist containing all the code

,

来自 brianberns 的公认答案确实提供了一种在玫瑰树上实现 cps 的替代方法。

我还添加了来自 Tomas Petricek 的 this alternative solution。它展示了我们如何通过将树的类型从单个节点更改为内部循环中的节点列表来消除额外的函数调用。

我应该使用术语多路树(我将在一分钟内更改)但至少这个问题现在记录了三种不同的方法。希望它会帮助其他人。