Помощь в проектировании древовидной структуры - Напряжение между функционалом и ООП - PullRequest
5 голосов
/ 22 августа 2009

Я изучал f # в предыдущие дни, писал небольшой проект, который, наконец, работает (конечно, с помощью SO).

Я пытаюсь научиться быть настолько идиоматичным, насколько это возможно, что в основном означает, что я стараюсь не изменять свои структуры данных. Это стоит мне много усилий :-) В своем поиске идиоматического функционального программирования я старался использовать как можно больше списков, кортежей и записей, а не объектов. Но потом «практичность побеждает чистоту», и поэтому на этот раз я переписываю свой маленький проект, используя объекты.

Я подумал, что вы могли бы дать мне несколько советов, конечно, моя идея "хорошего дизайна функционального программирования" еще не очень хорошо определена.

Например, мне нужно изменить узлы дерева, одновременно изменяя состояния на двух разных уровнях (L и L + 1). Я мог сделать это без изменения данных, но мне нужно было много «внутренних» и «вспомогательных» функций, с аккумуляторами и так далее. Приятное ощущение возможности четко выразить алгоритм было потеряно для меня из-за необходимости вносить изменения в мою структуру данных. Например, это очень просто в императивных языках: просто разыменуйте указатели на соответствующие узлы, измените их состояние и выполните итерации. Конечно, я не спроектировал должным образом свою структуру, и по этой причине я сейчас пробую подход ООП.

Я посмотрел на SICP, на Как разрабатывать программы и нашел тезис C. Okasaki («Чисто функциональные структуры данных»), но примеры на SICP и HTDP похожи на то, что я делал, или, может быть, я Я не в состоянии понять их полностью. Тезис, с другой стороны, сейчас слишком сложен для меня: -)

Что вы думаете об этом "напряжении", которое я испытываю? Я слишком строго интерпретирую «никогда не изменяющие данные»? Не могли бы вы предложить мне какой-нибудь ресурс?

Заранее спасибо, Франческо

Ответы [ 6 ]

6 голосов
/ 23 августа 2009

Когда дело доходит до «обновления дерева», я думаю, что вы всегда можете сделать это довольно элегантно используя катаморфизмы (складки над деревьями). У меня есть длинная серия блогов об этом, и большая часть приведенного ниже примера кода взята из части 4 серии .

При первом изучении я считаю, что лучше всего сосредоточиться на конкретном маленьком, конкретном постановка задачи. На основании вашего описания я придумал следующую задачу:

У вас есть двоичное дерево, где каждый узел содержит «имя» и «сумму» (может думать об этом как о банковских счетах или о чем-то подобном). И я хочу написать функцию который может сказать кому-то «украсть» определенную сумму от каждого из его прямых дети. Вот картинка, чтобы описать, что я имею в виду:

альтернативный текст http://oljksa.bay.livefilestore.com/y1pNWjpCPP6MbI3rMfutskkTveCWVEns5xXaOf-NZlIz2Hs_CowykUmwtlVV7bPXRwh4WHJMT-5hSuGVZEhmAIPuw/FunWithTrees.png

Слева у меня оригинальное дерево. Средний пример показывает результат, который я хочу, если узел «D» просят украсть «10» у каждого из его детей. И правильный пример показывает желаемый результат, если вместо этого я попросил «F» украсть «30» в исходном примере.

Обратите внимание, что используемая мной древовидная структура будет неизменной, а красные цвета в На диаграмме обозначены «новые узлы дерева» относительно исходного дерева. Это черный узлы используются совместно с исходной древовидной структурой (Object.ReferenceEquals для одного другой).

Теперь, предполагая типичную древовидную структуру, такую ​​как

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

и функцию "Steal" действительно легко написать, если у вас есть обычная "fold" шаблонный:

// 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 неизменного дерева, просто создав базовую логику. Вместо того, чтобы объяснить все это здесь, вы должны прочитать мой блог (по крайней мере, начало: части один два три четыре ).

Вот весь код (который нарисовал картинку выше):

// 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 
4 голосов
/ 22 августа 2009

Полагаю, если вы начнете свое предложение с "Я должен изменить узлы дерева, изменить одновременно состояния в двух разных уровни ", то вы не решаете свою проблему функционально. Это все равно, что написать статью на иностранном языке, сначала написав ее на своем родном языке, а затем пытаясь перевести. На самом деле не работает. Я знаю, это больно, но, на мой взгляд, лучше полностью погрузиться. Пока не беспокойтесь о сравнении подходов.

Один способ, которым я научился изучать «функциональный путь», - это посмотреть (и реализовать себя!) Некоторые функциональные жемчужины . Они в основном хорошо документируют элегантные программы с супер функциональными возможностями для решения различных задач. Начните со старых, и не бойтесь перестать читать и попробуйте другое, если вы его не получите. Просто вернитесь к этому позже с новым энтузиазмом и большим опытом. Это помогает:)

2 голосов
/ 22 августа 2009

Мне нужно изменить узлы дерева.

Нет, нет. Это ваша проблема.

Это стоит мне много усилий

Это типично. Нелегко научиться программировать с неизменными структурами данных. И большинству новичков это поначалу кажется неестественным. Это вдвойне сложно, потому что HTDP и SICP не дают хороших моделей для подражания (см. Сноску).

Я подумал, что вы могли бы дать мне несколько советов, конечно, моя идея «хорошего дизайна функционального программирования» еще не очень хорошо определена.

Мы можем, но вы должны сказать нам, в чем проблема . Тогда многие люди на этом форуме могут сказать вам, если это та проблема, решение которой можно выразить четко, не прибегая к мутации. Большинство проблем с деревом могут. Но с информацией, которую вы нам предоставили, мы не сможем сказать.

Я слишком строго интерпретирую "никогда не изменять данные"?

Не совсем достаточно, я бы сказал.

Пожалуйста, напишите вопрос, указывающий, какую проблему вы пытаетесь решить.


Сноска: и HTDP, и SICP выполнены в схеме, в которой отсутствует сопоставление с образцом. В этом случае понять код манипуляции с деревом на намного сложнее, чем при использовании сопоставления с шаблоном, предоставляемого F #. Насколько мне известно, сопоставление с образцом является важной функцией для написания понятного кода в чисто функциональном стиле. Что касается ресурсов, вы можете рассмотреть новую книгу Грэма Хаттона о Программирование на Хаскеле .

2 голосов
/ 22 августа 2009

Что вы думаете об этом "напряжении" что я испытываю? Я интерпретировать «никогда не мутировать данные» слишком строго? Не могли бы вы предложить мне какой-то ресурс?

По моему мнению, если вы изучаете функциональное программирование в первый раз, лучше всего начинать с изменяемого состояния с нуля. В противном случае вы в конечном итоге вернетесь к изменяемому состоянию в качестве первого средства, и весь ваш код F # будет C # с немного другим синтаксисом.

Что касается структур данных, некоторые из них легче выразить в функциональном стиле, чем другие. Не могли бы вы дать описание того, как вы пытаетесь изменить свое дерево?

На данный момент, я бы порекомендовал F # Страница Wikibook по структурам данных , чтобы увидеть, как структуры данных пишутся в функциональном стиле.

Я посмотрел на SICP, на Как проектировать программы и нашли диссертацию С. Окасаки («Чисто функциональные данные структуры ")

Я лично нашел книгу Окасаки более читабельной, чем тезис онлайн.

1 голос
/ 23 августа 2009

Взгляните на структуру данных Zipper .

0 голосов
/ 22 августа 2009

Например, я должен изменить узлы дерева, одновременно изменяя состояния на двух разных уровнях (L и L + 1)

Почему? На функциональном языке вы бы вместо этого создали новое дерево. Он может повторно использовать поддеревья, которые не нужно изменять, и просто подключить их к вновь созданному корню. «Не изменять данные» не означает «пытаться изменять данные, чтобы никто не заметил, и, добавив так много вспомогательных методов, что никто не осознает , что это то, что вы делаете».

Это означает «не изменяйте свои данные. Вместо этого создайте новые копии, которые инициализируются новыми, правильными значениями».

...