帮助设计树结构 - 功能和OOP之间的张力

时间:2009-08-22 11:45:09

标签: oop f# functional-programming

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

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

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

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

我看过SICP,如何设计程序,并找到了C. Okasaki的论文(“纯粹的功能数据结构”),但SICP和HTDP的例子与我的做法相似,或者我可能我无法完全理解它们。另一方面,论文对我来说有点太难了: - )

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

提前致谢, 弗朗西斯科

6 个答案:

答案 0 :(得分:6)

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

首次学习时,我发现最好将注意力放在特定的小型混凝土上 问题陈述。根据您的描述,我发明了以下问题:

你有一个二叉树,每个节点包含一个“名称”和一个“数量”(可以 把它想象成银行账户或其他一些东西。我想写一个函数 这可以告诉别人从他的每一个直接“窃取”一定数量 儿童。这是描述我的意思的图片:

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

在左边我有一棵原始树。中间的例子显示了我想要的结果节点 'D'被要求从他的每个孩子身上偷走'10'。正确的例子 如果相反我在原始例子中要求'F'窃取'30',则显示出所需的结果。

请注意,我使用的树结构将是不可变的,而红色则是 图表指定相对于原始树的“新树节点”。那是黑色的 节点与原始树结构共享(Object.ReferenceEquals为1) 另一个)。

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

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级别的算法 仅仅通过创建核心逻辑,不可变树的L + 1。而不是解释 在这里,您应该阅读我的博客系列(至少在开头:部分one two three four)。

以下是所有代码(如上图所示):

// 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 

答案 1 :(得分:4)

我想如果你用“我必须修改树的节点”开始你的句子,修改同时陈述在两个不同的层面“那么你并没有真正以功能的方式解决你的问题。这就像用一种外语写一篇论文,首先用你的母语写作,然后试着翻译。不起作用。我知道这很疼,但在我看来,最好完全沉浸在自己身上。不要担心比较这些方法。

我发现学习“功能方式”的一种方法是查看(并实现自己!)一些functional pearls。他们基本上是文档超级功能优雅的程序,以解决各种问题。从较旧的开始,如果你没有得到它,不要害怕停止阅读并尝试另一个。稍后再回到它,重新获得热情和更多经验。它有助于:)

答案 2 :(得分:2)

  

你怎么看待这种“紧张”   我遇到了什么?我   解释“从不改变数据”   太严格了?你能建议我吗?   一些资源?

在我看来,如果你是第一次学习函数式编程,最好从零可变状态开始。否则,你最终只会以可变状态作为你的第一个手段,而你所有的F#代码都将是C#,语法略有不同。

关于数据结构,有些比其他结构更容易表达。您能否提供一下如何修改树的说明?

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

  

我看过SICP,如何设计   节目并由C.发现了一篇论文。   Okasaki(“纯粹的功能数据   结构“)

我个人认为Okasaki's book比在线论文更具可读性。

答案 3 :(得分:2)

  

我必须修改树的节点。

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

  

这让我付出了很多努力

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

  

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

我们可以,但您必须告诉我们问题是什么。然后,这个论坛上的很多人都可以告诉你这是否是一种问题,其解决方案可以用一种明确的方式表达而不需要突变。大多数树问题都可以。但是根据你给我们的信息,我们无法告诉你。

  

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

不够严格,我会说。

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


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

答案 4 :(得分:1)

查看Zipper数据结构。

答案 5 :(得分:0)

  

例如,我必须修改树的节点,同时修改两个不同级别(L和L + 1)的状态

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

这意味着“不要改变你的数据。改为创建新的副本,用新的,正确的值初始化”。