【问题标题】:Help in designing a tree structure - Tension between functional and OOP帮助设计树结构 - 功能和 OOP 之间的张力
【发布时间】:2010-11-21 21:30:18
【问题描述】:

前几天我一直在学习 f#,写了一个小项目,终于可以工作了(当然是在 SO 的帮助下)。

我正在努力学习尽可能地习惯用法,这基本上意味着我尽量不改变我的数据结构。这让我付出了很多努力:-) 在寻找惯用的函数式编程时,我一直在尝试尽可能多地使用列表、元组和记录,而不是对象。但是“实用性胜过纯粹性”,所以这次我正在使用对象重写我的小项目。

我想你可以给我一些建议,当然我的“好的函数式编程设计”的想法还没有很好的定义。

例如,我必须修改树的节点,同时修改两个不同级别(L 和 L+1)的状态。我已经能够在不改变数据的情况下做到这一点,但是我需要很多“内部”和“辅助”函数,以及累加器等等。由于需要以一种复杂的方式修改我的数据结构,我失去了能够清楚地表达算法的美妙感觉。这在命令式语言中非常容易,例如:只需取消引用指向相关节点的指针,修改它们的状态并迭代。 当然我的结构设计不正确,因此我现在正在尝试 OOP 方法。

我在 How to design programs 中查看过 SICP,并找到了 C. Okasaki 的一篇论文(“纯函数式数据结构”),但 SICP 和 HTDP 上的示例与我所做的类似,或者也许我'我无法完全理解它们。另一方面,论文目前对我来说有点太难了:-)

您如何看待我正在经历的这种“紧张”?我是否过于严格地解释“从不变异数据”?你能给我推荐一些资源吗?

提前致谢, 弗朗切斯科

【问题讨论】:

    标签: oop f# functional-programming


    【解决方案1】:

    当谈到“树更新”时,我认为你总是可以非常优雅地做到这一点 使用catamorphisms(折叠在树上)。我有一个关于这个的长博客系列, 下面的示例代码大部分来自part 4 of the series

    第一次学习时,我发现最好专注于特定的小而具体的 问题陈述。根据您的描述,我发明了以下问题:

    你有一棵二叉树,其中每个节点都包含一个“名称”和一个“数量”(可以 把它想象成银行账户之类的)。我想写一个函数 这可以告诉某人从他的每个直接“窃取”一定数量 孩子们。这是一张图片来描述我的意思:

    alt text http://oljksa.bay.livefilestore.com/y1pNWjpCPP6MbI3rMfutskkTveCWVEns5xXaOf-NZlIz2Hs_CowykUmwtlVV7bPXRwh4WHJMT-5hSuGVZEhmAIPuw/FunWithTrees.png

    在左边我有一棵原始树。中间的例子显示了我想要的结果 if node “D”被要求从他的每个孩子那里偷“10”。和正确的例子 显示了如果我在原始示例中让 'F' 窃取 '30' 所期望的结果。

    请注意,我使用的树结构是不可变的,并且 图指定相对于原始树的“新树节点”。那是黑色 节点与原始树结构共享(Object.ReferenceEquals to one 另一个)。

    现在,假设一个典型的树结构,如

    type Tree<'T> =                          //'
        | Node of 'T * Tree<'T> * Tree<'T>   //'
        | Leaf
    

    我们将原始树表示为

    let origTree = Node(("D",1000),
                       Node(("B",1000),
                           Node(("A",1000),Leaf,Leaf),
                           Node(("C",1000),Leaf,Leaf)),
                       Node(("F",1000),
                           Node(("E",1000),Leaf,Leaf),
                           Leaf))
    

    而且“窃取”功能真的很容易编写,假设你有通常的“折叠” 样板:

    // have 'stealerName' take 'amount' from each of its children and
    // add it to its own value
    let Steal stealerName amount tree =
        let Subtract amount = function
            | Node((name,value),l,r) -> amount, Node((name,value-amount),l,r)
            | Leaf -> 0, Leaf
        tree |> XFoldTree 
            (fun (name,value) left right ->
                if name = stealerName then
                    let leftAmt, newLeft = Subtract amount left
                    let rightAmt, newRight = Subtract amount right
                    XNode((name,value+leftAmt+rightAmt),newLeft,newRight)
                else
                    XNode((name,value), left, right))
            XLeaf
    // examples
    let dSteals10 = Steal "D" 10 origTree
    let fSteals30 = Steal "F" 30 origTree
    

    就是这样,你已经完成了,你已经编写了一个“更新”级别 L 和 只需编写核心逻辑即可获得不可变树的 L+1。而不是解释 都在这里,你应该去阅读我的博客系列(至少开始:部分onetwothreefour)。

    这是所有代码(绘制上图):

    // Tree boilerplate
    // See http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!248.entry
    type Tree<'T> =
        | Node of 'T * Tree<'T> * Tree<'T>
        | Leaf
    let (===) x y = obj.ReferenceEquals(x,y)    
    let XFoldTree nodeF leafV tree =  
        let rec Loop t cont =  
            match t with  
            | Node(x,left,right) -> Loop left  (fun lacc ->   
                                    Loop right (fun racc ->  
                                    cont (nodeF x lacc racc t)))
            | Leaf -> cont (leafV t)
        Loop tree (fun x -> x)
    let XNode (x,l,r) (Node(xo,lo,ro) as orig) = 
        if xo = x && lo === l && ro === r then  
            orig 
        else 
            Node(x,l,r) 
    let XLeaf (Leaf as orig) = 
        orig
    let FoldTree nodeF leafV tree =  
        XFoldTree (fun x l r _ -> nodeF x l r) (fun _ -> leafV) tree
    // /////////////////////////////////////////
    // stuff specific to this problem
    let origTree = Node(("D",1000),
                       Node(("B",1000),
                           Node(("A",1000),Leaf,Leaf),
                           Node(("C",1000),Leaf,Leaf)),
                       Node(("F",1000),
                           Node(("E",1000),Leaf,Leaf),
                           Leaf))
    
    // have 'stealerName' take 'amount' from each of its children and
    // add it to its own value
    let Steal stealerName amount tree =
        let Subtract amount = function
            | Node((name,value),l,r) -> amount, Node((name,value-amount),l,r)
            | Leaf -> 0, Leaf
        tree |> XFoldTree 
            (fun (name,value) left right ->
                if name = stealerName then
                    let leftAmt, newLeft = Subtract amount left
                    let rightAmt, newRight = Subtract amount right
                    XNode((name,value+leftAmt+rightAmt),newLeft,newRight)
                else
                    XNode((name,value), left, right))
            XLeaf
    let dSteals10 = Steal "D" 10 origTree
    let fSteals30 = Steal "F" 30 origTree
    
    // /////////////////////////////////////////
    // once again,
    // see http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!248.entry
    
    // DiffTree: Tree<'T> * Tree<'T> -> Tree<'T * bool> 
    // return second tree with extra bool 
    // the bool signifies whether the Node "ReferenceEquals" the first tree 
    let rec DiffTree(tree,tree2) = 
        XFoldTree (fun x l r t t2 ->  
            let (Node(x2,l2,r2)) = t2 
            Node((x2,t===t2), l l2, r r2)) (fun _ _ -> Leaf) tree tree2 
    
    open System.Windows 
    open System.Windows.Controls 
    open System.Windows.Input 
    open System.Windows.Media 
    open System.Windows.Shapes 
    
    // Handy functions to make multiple transforms be a more fluent interface 
    let IdentT() = new TransformGroup() 
    let AddT t (tg : TransformGroup) = tg.Children.Add(t); tg 
    let ScaleT x y (tg : TransformGroup) = tg.Children.Add(new ScaleTransform(x, y)); tg 
    let TranslateT x y (tg : TransformGroup) = tg.Children.Add(new TranslateTransform(x, y)); tg 
    
    // Draw: Canvas -> Tree<'T * bool> -> unit 
    let Draw (canvas : Canvas) tree = 
        // assumes canvas is normalized to 1.0 x 1.0 
        FoldTree (fun ((name,value),b) l r trans -> 
            // current node in top half, centered left-to-right 
            let tb = new TextBox(Width=100.0, Height=100.0, FontSize=30.0, Text=sprintf "%s:%d" name value, 
                                 // the tree is a "diff tree" where the bool represents 
                                 // "ReferenceEquals" differences, so color diffs Red 
                                 Foreground=(if b then Brushes.Black else Brushes.Red),  
                                 HorizontalContentAlignment=HorizontalAlignment.Center, 
                                 VerticalContentAlignment=VerticalAlignment.Center) 
            tb.RenderTransform <- IdentT() |> ScaleT 0.005 0.005 |> TranslateT 0.25 0.0 |> AddT trans 
            canvas.Children.Add(tb) |> ignore 
            // left child in bottom-left quadrant 
            l (IdentT() |> ScaleT 0.5 0.5 |> TranslateT 0.0 0.5 |> AddT trans) 
            // right child in bottom-right quadrant 
            r (IdentT() |> ScaleT 0.5 0.5 |> TranslateT 0.5 0.5 |> AddT trans) 
        ) (fun _ -> ()) tree (IdentT()) 
    
    let TreeToCanvas tree =
        let canvas = new Canvas(Width=1.0, Height=1.0, Background = Brushes.Blue, 
                                LayoutTransform=new ScaleTransform(400.0, 400.0)) 
        Draw canvas tree
        canvas
    
    let TitledControl title control =
        let grid = new Grid()
        grid.ColumnDefinitions.Add(new ColumnDefinition())
        grid.RowDefinitions.Add(new RowDefinition())
        grid.RowDefinitions.Add(new RowDefinition())
        let text = new TextBlock(Text = title, HorizontalAlignment = HorizontalAlignment.Center)
        Grid.SetRow(text, 0)
        Grid.SetColumn(text, 0)
        grid.Children.Add(text) |> ignore
        Grid.SetRow(control, 1)
        Grid.SetColumn(control, 0)
        grid.Children.Add(control) |> ignore
        grid
    
    let HorizontalGrid (controls:_[]) =
        let grid = new Grid()
        grid.RowDefinitions.Add(new RowDefinition())
        for i in 0..controls.Length-1 do
            let c = controls.[i]
            grid.ColumnDefinitions.Add(new ColumnDefinition())
            Grid.SetRow(c, 0)
            Grid.SetColumn(c, i)
            grid.Children.Add(c) |> ignore
        grid
    
    type MyWPFWindow(content, title) as this = 
        inherit Window()
    
        do  
            this.Content <- content
            this.Title <- title
            this.SizeToContent <- SizeToContent.WidthAndHeight  
    
    [<System.STAThread()>] 
    do  
        let app =  new Application() 
        let controls = [|
            TitledControl "Original" (TreeToCanvas(DiffTree(origTree,origTree)))
            TitledControl "D steals 10" (TreeToCanvas(DiffTree(origTree,dSteals10)))
            TitledControl "F steals 30" (TreeToCanvas(DiffTree(origTree,fSteals30))) |]
        app.Run(new MyWPFWindow(HorizontalGrid controls, "Fun with trees")) |> ignore 
    

    【讨论】:

    • 布赖恩真是太好了。谢谢!明天我将详细研究它(可能还会出现其他问题),但我开始了解你是如何让它工作的(所谓的“通常的“折叠”样板文件)。我开始知道这在我的情况下是如何工作的 :-) 感谢您提供指向您博客文章的指针。
    【解决方案2】:

    看看Zipper数据结构。

    【讨论】:

      【解决方案3】:

      我必须修改树的节点。

      不,你没有。那是你的问题。

      这让我付出了很多努力

      这是典型的。学习使用不可变数据结构进行编程并不容易。对于大多数初学者来说,一开始这似乎很不自然。这是加倍困难的,因为 HTDP 和 SICP 没有给你很好的模型来遵循(见脚注)。

      我想你可以给我一些建议,当然我的“好的函数式编程设计”的想法还没有很好地定义。

      我们可以,但您必须告诉我们问题出在哪里。那么这个论坛上的很多人都会告诉你,这是否是一种无需借助变异就可以清楚地表达解决方案的问题。大多数树问题都可以。但以你提供给我们的信息,我们无从得知。

      我是否过于严格地解释“从不改变数据”?

      我会说不够严格。

      请发布一个问题,说明您要解决的问题。


      脚注:HTDP 和 SICP 都是在 Scheme 中完成的,缺少模式匹配。在这种情况下,理解树操作代码比使用 F# 提供的模式匹配更难。就我而言,模式匹配是以纯函数式风格编写清晰代码的基本特征。对于资源,您可以考虑 Graham Hutton 关于Programming in Haskell 的新书。

      【讨论】:

        【解决方案4】:

        比如我要修改一棵树的节点,同时修改两个不同级别(L和L+1)的状态

        为什么?在函数式语言中,您将创建一个新树。它可以重用不需要修改的子树,只需将它们插入到新创建的根中即可。 “不要改变数据”并不意味着“尝试在没有任何人注意的情况下改变数据,并通过添加如此多的帮助方法而没有人意识到这就是你正在做的事情”。 p>

        它的意思是“不要改变你的数据。而是创建新的副本,用新的、正确的值初始化”。

        【讨论】:

        • -1:尽管在错误的设置中使用了“修改”这个词,但很明显 OP 正在尝试编写一个不可变的树,否则就没有必要谈论累积参数或将他的经验与可变树进行对比。告诉他创建新树并没有什么用,因为他从一开始就一直在尝试这样做,但只是努力以惯用和可读的方式编写它。
        【解决方案5】:

        我猜如果你以“我必须修改树的节点,修改同时状态开始你的句子在两个不同的层面上”,那么您并没有真正以功能性的方式解决您的问题。这就像用外语写一篇论文,先用母语写,然后再翻译。真的不行。我知道这很痛苦,但在我看来,最好完全沉浸在自己之中。暂时不用担心比较这些方法。

        我发现学习“功能性方法”的一种方法是查看(并自己实现!)一些functional pearls。它们基本上很好地记录了解决各种问题的超功能优雅程序。从较旧的开始,如果你不明白,不要害怕停止阅读并尝试另一个。稍后再以新的热情和更多的经验回来。它有帮助:)

        【讨论】:

        • 感谢指向功能珍珠的指针。我会努力研究它们。我使用“修改”,因为这就是我(或标准书籍/论文......)描述算法的方式:首先你构建一棵树,每个节点都有一些状态,然后你迭代它并改变每个节点的状态所以一些财产是有保障的。我喜欢你用外语做类比。这正是我正在经历的压力:多年的“其他”方法实践,现在我花了一整天的时间来获得一些感觉......没有功能的东西。
        • 是的,尤其是数据结构几乎总是使用可变状态来描述。如果您想拥有另一种观点,Okasaki 的书是一本不错的书,但如果您(像我一样)对数据结构本身不感兴趣,那么这本书的阅读量就相当大了。
        【解决方案6】:

        您如何看待这种“紧张” 我正在经历什么?我是不是 解释“从不改变数据” 太严格了?你能建议我吗 一些资源?

        在我看来,如果您是第一次学习函数式编程,最好从零可变状态开始。否则,您最终只能求助于可变状态作为您的第一选择,并且您的所有 F# 代码都将是 C#,语法略有不同。

        关于数据结构,有些数据结构比其他数据结构更容易以函数式风格表达。您能否描述一下您是如何尝试修改您的树的?

        目前,我推荐F# Wikibook's page on data structures 看看数据结构是如何以函数式风格编写的。

        我看过 SICP,在 How to design 程序并找到了 C. 冈崎(“纯功能数据 结构")

        我个人发现Okasaki's book 比网上的论文更具可读性。

        【讨论】:

        • 我同意,但想提出一个不同的意见,如果你不介意花很长时间,慢慢地将函数式编程混合到你的命令式程序中最终会奏效。在切换到 Scheme 和 Haskell 之前,我是在 Common Lisp 和 Python 中完成的。不过,一个明显的转变,之后你的设计就纯粹是功能性的了,所以如果你想尽快完成这个转变,也许前期多一些痛苦会更好。
        • @Juliet:感谢您提供指向 wikibooks 的指针。出于某种原因,我错过了那个页面......我有意识地限制自己避免可变状态。我什至不记得正确的 f# 语法,只是为了避免诱惑 :-) 通过说“更多 OOP 方法”,无论如何我都是在谈论不可变对象。我将尝试以简洁但(希望)足够清晰的方式描述我正在使用的方法,但现在它有点太晚了:-)
        • @Nathan:谢谢你的意见,我明白你的意思。为了我的快乐,我正在研究 f# 作为一项智力挑战。编写“惯用”代码,而不是从我知道的其他语言翻译。
        猜你喜欢
        • 2016-05-08
        • 1970-01-01
        • 2011-03-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-01-11
        • 1970-01-01
        相关资源
        最近更新 更多