zwilias / elm-rosetree / Tree.Diff

Diffing and merging trees

The Tree.Diff module offers datastructures and function to handle diffing and merging trees. A diff represents the abstract action that need to be taken to go from one tree to another. Merging trees allows actually executing these actions.

Data types


type Diff a
    = Keep (Tree a)
    | Replace (Tree a) (Tree a)
    | Copy a (List (Diff a)) (Tail a)

Either nothing changed, and we can keep a (sub)tree, or something changed.

When something changed, there are essentially 2 cases:

In case there was a different with the children, there is also the possibility that the length was different. This is described in the Tail of the Copy.


type Tail a
    = Left (List (Tree a))
    | Right (List (Tree a))
    | Empty

If the left node had more children than the right now, we get a Left tail with the trailing children from the left tree.

On the other hand, if the right node had more children we get a Right tail.

If both trees has the same number of children, the tail is Empty.

Diffing

diff : Tree a -> Tree a -> Diff a

Diffing 2 trees (using standard equivalence (==)) produces a Diff!

import Tree.Diff as Diff
import Tree exposing (tree, singleton)

Diff.diff
    (tree "root"
        [ tree "folder"
            [ singleton "foo"
            , singleton "bar"
            ]
        , singleton "yeah"
        , singleton "keep me!"
        ]
    )
    (tree "root"
        [ tree "folder"
            [ singleton "foo" ]
        , tree "folder2"
            [ singleton "nice" ]
        , singleton "keep me!"
        ]
    )
--> Diff.Copy "root"
-->     [ Diff.Copy "folder"
-->         [ Diff.Keep (singleton "foo") ]
-->         (Diff.Left [ singleton "bar" ])
-->     , Diff.Replace
-->         (singleton "yeah")
-->         (tree "folder2" [ singleton "nice" ])
-->     , Diff.Keep (singleton "keep me!")
-->     ]
-->     Diff.Empty

diffWith : (a -> a -> Basics.Bool) -> Tree a -> Tree a -> Diff a

Diff using custom equivalence.

This allows using a custom function to decide whether two labels are really equivalent. Perhaps you're using some custom datatype and you consider two instances of them to be equivalent if they hold the same data, regardless of their structural equality? Or perhaps your labels are floats, and you want to check using some epsilon value?

This is your function!

diffBy : (a -> b) -> Tree a -> Tree a -> Diff a

Diff using regular equality on a derived property of the label.

This is related to diffWith in the same way List.sortBy is related to List.sortWith. Imagine, for example, that your labels are tuples and you're only interested in the second value.

You could either write diffWith (\(_, x) (_, y) -> x == y) left right or the equivalent but much simple diffBy Tuple.second.

If you find yourself being worried about performance: Please benchmark!

Merging

Note: Merging trees according to the diff structure described here using regular equality on the labels will always result in the second tree being returned. For that reason, only mergeWith and mergeBy exist: merge a b = b feels like a silly function to offer!

mergeWith : (a -> a -> Basics.Bool) -> Tree a -> Tree a -> Tree a

mergeBy : (a -> b) -> Tree a -> Tree a -> Tree a

Has the same relation to mergeWith as diffBy has to diffWith.

import Tree.Diff as Diff
import Tree exposing (tree, singleton)

Diff.mergeBy Tuple.second
    (tree ( 1, "root" )
        [ tree ( 1, "folder" )
            [ singleton ( 1, "foo" )
            , singleton ( 1, "bar" )
            ]
        , singleton ( 1, "yeah" )
        , singleton ( 1, "keep me!" )
        ]
    )
    (tree ( 2, "root" )
        [ tree ( 2, "folder" )
            [ singleton ( 2, "foo" ) ]
        , tree ( 2, "folder2" )
            [ singleton ( 2, "nice" ) ]
        , singleton ( 2, "keep me!" )
        ]
    )
--> tree ( 1, "root" )
-->     [ tree ( 1, "folder" )
-->         [ singleton ( 1, "foo" ) ]
-->     , tree ( 2, "folder2" )
-->         [ singleton ( 2, "nice" ) ]
-->     , singleton ( 1, "keep me!" )
-->     ]