在本地编辑纯功能树

Phi*_*sky 10 tree immutability zipper purely-functional

让我们定义一个树T:

    A
   / \
  B   C
 / \
D   E
Run Code Online (Sandbox Code Playgroud)

假设一个新节点被添加到E,产生T':

    A
   / \
  B   C
 / \
D   E
     \
      G
Run Code Online (Sandbox Code Playgroud)

在一个可变的语言中,这是一个简单的任务 - 只需更新E的孩子,我们就完成了.然而,在一个不可变的世界中,有必要首先知道E的路径,然后从E +新子导出E',然后导出B',然后导出A'(= T').

这很麻烦; 理想情况下,会有一些函数可以获取E和G(可能还有T)的值并产生T',而不提供到E的路径.

我看到两种可能的方法来解决这个问题:

  • 父引用 - 这样,每个节点都能够派生到根路径.两个问题:创建两个相互引用的节点(即父< - >子)是一个纯功能语言的问题(任何简单的解决方案?); 每当E - > E'派生出来时,所有 E'的孩子也需要新派生,因为他们现在存储E的旧值而不是E'.
  • zippers - 每个节点在创建时存储拉链,从其父拉链派生.相互引用的问题消失了,但是,当导出E - > E'时,所有E'的儿童拉链也需要派生,因为它们现在指向旧树E.

考虑到合理的表现,我想要的是什么?非常感谢任何输入!

Lyn*_*ite 1

另一个选项,基于进行延迟替换。如果它对性能至关重要并且需要对其进行大量更改,我建议对其进行基准测试。

我已经在 F# 中实现了它,但是我认为除了打印功能之外我没有使用任何“不纯粹”的东西。

这是一段文字墙,基本原理是保持树的惰性,通过替换返回节点的函数来替换节点。

诀窍是您需要某种方法来识别节点,这不是它自己的引用/名称,也不是通过值。标识必须可复制到 ReplacementNodes 上。在本例中,我使用了 System.Object,因为它们在引用上各自不同。

type TreeNode<'a> = {
    getChildren: unit -> seq<TreeNode<'a>>;
    value: 'a;
    originalRefId: System.Object; //This is set when the onject is created,
                                  // so we can identify any nodes that are effectivly this one 
    }


let BasicTreeNode : 'a ->seq<TreeNode<'a>>-> TreeNode<'a>  = fun nodeValue -> fun children ->
    {value = nodeValue; originalRefId = System.Object(); getChildren = fun () -> children;}


let rec ReplacementNode : TreeNode<'a> -> TreeNode<'a> -> TreeNode<'a> -> TreeNode<'a>  = 
    fun nodeToReplace -> fun newNode -> fun baseNode ->
        if (System.Object.ReferenceEquals(baseNode.originalRefId, nodeToReplace.originalRefId)) then
            //If it has the same Oringal 
            newNode //replace the node
        else
            //Just another pass on node, tranform its children, keep orignial reference
            {value = baseNode.value; 
             originalRefId = baseNode.originalRefId;
             getChildren = fun () -> 
                baseNode.getChildren() |> Seq.map(ReplacementNode nodeToReplace newNode); }


type TreeType<'a> = {
    Print: unit -> unit; 
    ReplaceNode: TreeNode<'a> -> TreeNode<'a> -> TreeType<'a>;
    //Put all the other tree methods, like Traversals, searches etc in this type
    }

let rec Tree  = fun rootNode ->
    {
        Print = fun () -> 
            let rec printNode = fun node -> fun depth -> 
                printf "%s %A\n" (String.replicate depth " - ")  node.value
                for n in node.getChildren() do printNode n (depth + 1)
            printNode rootNode 0
            ;
        ReplaceNode = fun oldNode -> fun newNode ->
            Tree (ReplacementNode oldNode newNode rootNode)



    }
Run Code Online (Sandbox Code Playgroud)

测试用例/示例:

let e = BasicTreeNode "E" Seq.empty
let d = BasicTreeNode "D" Seq.empty
let c = BasicTreeNode "C" Seq.empty
let b = BasicTreeNode "B" [d;e]
let a = BasicTreeNode "A" [b;c]
let originalTree = Tree a
printf "The Original Tree:\n"
originalTree.Print()

let g = BasicTreeNode "G" Seq.empty
let newE = BasicTreeNode "E" [g]

let newTree = originalTree.ReplaceNode e newE
printf "\n\nThe Tree with a Local Change: \n"
newTree.Print()

printf "\n\nThe Original Tree is Unchanged: \n"
originalTree.Print()


printf "\n\nThe Tree with a Second Local Change: \n"
let h = BasicTreeNode "H" Seq.empty
let newC = BasicTreeNode "C" [h]
let newTree2 = newTree.ReplaceNode c newC
newTree2.Print()



printf "\n\nTrying to Change a node that has been replaced doesn't work \n"
let i = BasicTreeNode "i" Seq.empty
let newnewC = BasicTreeNode "C" [h; i]
let newTree3 = newTree.ReplaceNode c newC //newTree.ReplaceNode newc newnewC would work
newTree3.Print()
Run Code Online (Sandbox Code Playgroud)

我们在测试结束时看到,使用旧节点名称(/引用)来替换对象是行不通的。可以选择创建一个具有另一个节点的引用 ID 的新类型:

//Like a basicTreeNode, but reuses an existing ID, so they can be replaced for oneanother
let EdittedTreeNode = fun orignalNode -> fun nodeValue -> fun children -> 
    {value = nodeValue; originalRefId = orignalNode.originalRefId; getChildren = fun () -> children;}
Run Code Online (Sandbox Code Playgroud)

您还可以编辑ReplacementNode定义,以便保留它所替换的节点的 ID。(不仅返回newNode,而是返回另一个具有value、 和getChildren的新节点newNode,但originalRefId返回 的nodetoReplace