A multiway tree or rosetree is a labeled tree where each node can have zero, one or more children, each of which represents a tree in its own right.
The root of the tree is always labeled, so a tree always has at least one label.
As an example, such a structure could represent a directory structure:
tree "root"
[ tree "home"
[ tree "user1" []
, tree "user2" []
]
, tree "etc" []
, tree "var"
[ tree "log" []
]
]
In a sense, Html msg
is pretty similar to how such trees look, but they can be
used to represent other things. A nested menu structure, or a sitemap, or any
other structure where a single root is connected to children which can each have
children of their own, and so on.
Represents a multiway tree. Each node in the tree holds a piece of
information (the label
) and a list of children, each of which is a tree.
Note: The constructors here are exposed as this can be sometimes easier to program than having to use the functions provided and can be an easy way to unblock yourself to build tree processing algorithms.
However the naive way of building tree algorithms is not stack safe and can cause crashes for large tree inputs.
For instance, a map
function can trivially be defined as:
map : (a -> b) -> Tree a -> Tree b
map fn (Tree l c) =
Tree (fn l) (List.map (map fn) c)
However, running this definition on a large tree can easily throw a
Uncaught RangeError: Maximum call stack size exceeded
.
Instead, it is often better to look at this libraries generic traversal functions, which can provide a stack safe implementation without too much overhead:
map : (a -> b) -> Tree a -> Tree b
map fn t =
Tree.depthFirstTraversal
-- pass through state, apply fn to label, pass through children
(\s a l c -> ( s, fn l, c ))
-- pass through state, reassemble tree
(\s a l c -> ( s, Tree.tree l c ))
-- we don't really care about state
()
t
-- discard state
|> Tuple.second
With this in mind, it's certainly OK to unblock yourself with the simpler version!
singleton : a -> Tree a
Creates a singleton tree. This corresponds to tree v []
.
singleton 5
|> label
--> 5
singleton "foo"
|> children
--> []
tree : a -> List (Tree a) -> Tree a
Construct a tree from a label and a list of children.
tree 5 []
--> singleton 5
tree 5
[ singleton 1
, singleton 2
, tree 3
[ singleton 4
, singleton 5
]
]
|> length
--> 6
label : Tree a -> a
Gives you the label of a tree.
tree "hello" [ singleton "world", singleton "etc" ]
|> label
--> "hello"
children : Tree a -> List (Tree a)
Returns the children of a tree as a list.
singleton "heh"
|> children
--> []
tree "hello" [ singleton "world", singleton "etc" ]
|> children
--> [ singleton "world", singleton "etc" ]
updateLabel : (a -> a) -> Tree a -> Tree a
Execute a function on the label of this tree.
tree "hello" [ singleton "world", singleton "etc" ]
|> updateLabel String.toUpper
--> tree "HELLO" [ singleton "world", singleton "etc" ]
replaceLabel : a -> Tree a -> Tree a
Replace the label of this tree.
singleton "foo"
|> replaceLabel "bar"
--> singleton "bar"
updateChildren : (List (Tree a) -> List (Tree a)) -> Tree a -> Tree a
Execute a function on the children of a tree.
tree "lower1"
[ singleton "upper1"
, tree "upper2" [ singleton "lower2"]
, singleton "upper3"
]
|> updateChildren (List.map (updateLabel String.toUpper))
--> tree "lower1"
--> [ singleton "UPPER1"
--> , tree "UPPER2" [ singleton "lower2"]
--> , singleton "UPPER3"
--> ]
replaceChildren : List (Tree a) -> Tree a -> Tree a
Replace the children of a tree.
tree "hello" [ singleton "world" ]
|> replaceChildren [ singleton "everyone" ]
--> tree "hello" [ singleton "everyone" ]
prependChild : Tree a -> Tree a -> Tree a
Prepend a single child to a tree.
tree "hello" [ singleton "everyone" ]
|> prependChild (singleton "dear")
--> tree "hello" [ singleton "dear", singleton "everyone" ]
appendChild : Tree a -> Tree a -> Tree a
Append a child to a tree. Note that this uses children ++ [ newChild ]
under the hood so use sparingly.
tree "hello" [ singleton "you" ]
|> appendChild (singleton "and you!")
--> tree "hello" [ singleton "you", singleton "and you!" ]
length : Tree a -> Basics.Int
Count the labels in a tree.
singleton "foo"
|> length
--> 1
tree "foo" [ singleton "bar", singleton "baz" ]
|> length
--> 3
depth : Tree a -> Basics.Int
Counts the number of levels in a tree (where the root is 0).
depth (tree 2 [ tree 1 [tree 0 []]])
--> 2
foldl : (a -> b -> b) -> b -> Tree a -> b
Fold over all the labels in a tree, left to right, depth first.
tree "Hello "
[ singleton "world "
, tree "and "
[ singleton "you "
, singleton "and "
, singleton "you"
]
, singleton "!"
]
|> foldl (\label acc -> acc ++ label) ""
--> "Hello world and you and you!"
foldr : (a -> b -> b) -> b -> Tree a -> b
Fold over all the labels in a tree, right to left, depth first.
tree 1
[ singleton 2
, tree 3
[ singleton 4
, singleton 5
]
, singleton 6
]
|> foldr (::) []
--> [ 1, 2, 3, 4, 5, 6 ]
toList : Tree a -> List a
Flattens the tree into a list. This is equivalent to foldr (::) []
leaves : Tree a -> List a
Returns the nodes that have no children.
tree 1
[ singleton 2
, tree 3
[ singleton 4
, tree 5
[ singleton 6]
]
, singleton 7
]
|> leaves
--> [ 2, 7, 4, 6 ]
links : Tree a -> List ( a, a )
Returns pairs representing parent-child relationships in the tree.
The left item is the label of the parent, the right item is the label of the child. Useful for visualising trees.
tree 1
[ singleton 2
, tree 3
[ singleton 4
, tree 5
[ singleton 6]
]
, singleton 7
]
|> links
--> [ ( 1, 2 ), ( 1, 3 ), ( 1, 7 ), ( 3, 4 ), ( 3, 5 ), ( 5, 6 ) ]
map : (a -> b) -> Tree a -> Tree b
Run a function on every label in the tree.
tree 1
[ singleton 2
, tree 3 [ singleton 4 ]
, singleton 5
]
|> map (\x -> String.fromInt (x * 2))
--> tree "2"
--> [ singleton "4"
--> , tree "6" [ singleton "8" ]
--> , singleton "10"
--> ]
indexedMap : (Basics.Int -> a -> b) -> Tree a -> Tree b
Run a function on every label in the tree while getting access to the
"index" of the label. This looks at thing in the same order as foldl
.
tree "foo"
[ singleton "bar"
, tree "baz" [ singleton "hello", singleton "world" ]
, singleton "qlux"
]
|> indexedMap (\idx val -> String.fromInt idx ++ " - " ++ val)
--> tree "0 - foo"
--> [ singleton "1 - bar"
--> , tree "2 - baz"
--> [ singleton "3 - hello"
--> , singleton "4 - world"
--> ]
--> , singleton "5 - qlux"
--> ]
mapAccumulate : (s -> a -> ( s, b )) -> s -> Tree a -> ( s, Tree b )
Map a function over every node while accumulating some value.
tree 1
[ singleton 2
, tree 3 [ singleton 4 ]
]
|> mapAccumulate (\acc label -> ( acc + label, String.fromInt label)) 0
--> ( 10
--> , tree "1"
--> [ singleton "2"
--> , tree "3" [ singleton "4" ]
--> ]
--> )
map2 : (a -> b -> c) -> Tree a -> Tree b -> Tree c
Map over 2 trees. Much like List.map2
, the result will be truncated to the shorter result.
left : Tree Int
left =
tree 3
[ singleton 5
, tree 6 [ singleton 12 ]
, singleton 4
]
right : Tree Int
right =
tree 8
[ tree 5 [ singleton 9 ]
, singleton 3
]
map2 (\x y -> x + y) left right
--> tree 11
--> [ singleton 10
--> , singleton 9
--> ]
indexedMap2 : (Basics.Int -> a -> b -> c) -> Tree a -> Tree b -> Tree c
Like map2
, but with the "index" added as the first argument.
mapAccumulate2 : (s -> a -> b -> ( s, c )) -> s -> Tree a -> Tree b -> ( s, Tree c )
Allows mapping over 2 trees while also accumulating a value.
left : Tree Int
left =
tree 3
[ singleton 5
, tree 6 [ singleton 12 ]
, singleton 4
]
right : Tree Int
right =
tree 8
[ tree 5 [ singleton 9 ]
, singleton 3
]
mapAccumulate2 (\sum x y -> ( sum + x + y, x + y )) 0 left right
--> ( 30
--> , tree 11
--> [ singleton 10
--> , singleton 9
--> ]
--> )
andMap : Tree a -> Tree (a -> b) -> Tree b
Given a tree of functions and a tree of values, applies the functions to the matching labels in the tree of values, truncating branches to match the common shape of the trees.
sumUp : (a -> b) -> (a -> List b -> b) -> Tree a -> Tree b
Oftentimes trees that represent some hierarchical organization only have real values at the leaves of the tree, but synthetic values are valuable to derive at other levels. For instance in a tree representing a filesystem of a software project, only files have lines of code, directories don't. But to visualize and understand the project it makes sense to assign to each directory the total lines contained in all the files inside it.
tree 0
[ singleton 32
, tree 0
[ singleton 221
, singleton 44
]
]
|> sumUp identity (always List.sum)
--> tree 297
--> [ singleton 32
--> , tree 265
--> [ singleton 221
--> , singleton 44
--> ]
--> ]
The first argument can be used to transform leaf nodes. For instance a more typically Elm representation would look like this:
tree Nothing
[ singleton (Just 32)
, tree Nothing
[ singleton (Just 221)
, singleton (Just 44)
]
, tree (Just 111)
[ singleton Nothing
]
]
|> sumUp (Maybe.withDefault 0) (\l c -> Maybe.withDefault (List.sum c) l)
--> tree 408
--> [ singleton 32
--> , tree 265
--> [ singleton 221
--> , singleton 44
--> ]
--> , tree 111
--> [ singleton 0
--> ]
--> ]
find : (Tree a -> Basics.Bool) -> Tree a -> Maybe (Tree a)
Finds a subtree whose label matches the predicate.
Searches the tree in a breadth-first manner.
tree 1
[ tree 3
[ singleton 5
, singleton 4
]
, singleton 2
, singleton 6
]
|> find (\a -> label a == 3)
--> Just (tree 3 [ singleton 5, singleton 4 ])
sortBy : (Tree a -> comparable) -> Tree a -> Tree a
Sorts all children of each node based on the accessor function.
sortWith : (List a -> Tree a -> Tree a -> Basics.Order) -> Tree a -> Tree a
Sorts all children of each node based on the comparator function (the function recieves a list of ancestors).
tree 1
[ tree 3
[ singleton 5
, singleton 4
]
, singleton 2
, singleton 6
]
|> sortWith (\_ a b -> compare (label a) (label b))
--> tree 1
--> [ singleton 2
--> , tree 3
--> [ singleton 4
--> , singleton 5
--> ]
--> , singleton 6
--> ]
unfold : (b -> ( a, List b )) -> b -> Tree a
Create a tree from a seed.
Running the function on the seed should return a label and a list of seeds to use for the children.
For example, this function takes and int, and uses the string representation of that int as the label, with its children representing the integers from 0 up to but not including the value. The expected result is a tree in which each label has the number of children mentioned in the label, recursively.
unfolder : Int -> (String, List Int)
unfolder x =
( String.fromInt x, List.range 0 (x - 1) )
unfold unfolder 3
--> tree "3"
--> [ singleton "0"
--> , tree "1" [ singleton "0" ]
--> , tree "2"
--> [ singleton "0"
--> , tree "1" [ singleton "0" ]
--> ]
--> ]
stratify : { id : a -> comparable, parentId : a -> Maybe comparable, transform : a -> b } -> List a -> Result (StratifyError b) (Tree b)
It is fairly common for tree data to be stored flattened (for instance in database tables or CSV files) into lists. A common way this is done is for each node in the tree to store the ID of its parent.
Stratify helps you recover the tree structure from data stored like this.
[ ( "root", Nothing )
, ( "leaf1", Just "root" )
, ( "branch", Just "root" )
, ( "leaf2", Just "branch" )
, ( "leaf3", Just "branch" )
]
|> stratify { id = Tuple.first, parentId = Tuple.second, transform = Tuple.first }
--> Ok (tree "root"
--> [ singleton "leaf1"
--> , tree "branch"
--> [ singleton "leaf2"
--> , singleton "leaf3"
--> ]
--> ]
--> )
stratify
tries to convert the entire
dataset into a tree. If that cannot be achieved, it returns the
following errors:
MultipleRoots a a
is returned when there is more than one node
where .parentId
returns Nothing
. The error contains the first
two nodes that had such property.
[ ( "root1", Nothing )
, ( "root2", Nothing)
]
|> stratify { id = Tuple.first, parentId = Tuple.second, transform = Tuple.first }
--> Err (MultipleRoots "root1" "root2")
NoRoot
is returned when there is no node where .parentId
returns
Nothing
(or an empty list is passed)
[ ( "leaf1", Just "root" )
, ( "leaf2", Just "root" )
]
|> stratify { id = Tuple.first, parentId = Tuple.second, transform = Tuple.first }
--> Err NoRoot
NodeItsOwnParent a
happens when .id
is the same as .parentId
for a node:
[ ("loop", Just "loop") ]
|> stratify { id = Tuple.first, parentId = Tuple.second, transform = Tuple.first }
--> Err (NodeItsOwnParent "loop")
DuplicatedId a
happens when there are two (or more) elements that return the same
.id
.
[ ( "root", Nothing )
, ( "xxx", Just "root" )
, ( "xxx", Just "root" )
]
|> stratify { id = Tuple.first, parentId = Tuple.second, transform = Tuple.first }
--> Err (DuplicateId "xxx")
DisconnectedNodes (Tree a)
happens when not all input items connect to the tree
(i.e. there are "orphan" items). This is an error where a tree was constructed, but
this error signals that some of the data is missing in the resulting tree.
[ ( "root", Nothing )
, ( "leaf1", Just "root" )
, ( "leaf2", Just "root" )
, ( "orphan", Just "nobody" )
]
|> stratify { id = Tuple.first, parentId = Tuple.second, transform = Tuple.first }
--> Err (DisconnectedNodes (
--> tree "root"
--> [ singleton "leaf1", singleton "leaf2" ]
--> ))
stratifyWithPath : { path : a -> List comparable, createMissingNode : List comparable -> a } -> List a -> Result (List comparable) (Tree a)
The other way tree data is sometimes represented in flat formats is that each item stores a path to the root. Common examples are filesystems where we see file paths such as "foo/bar/baz.txt" or DNS "my.domain.com".
This function will recreate the tree based on these paths:
[ "Tree"
, "Tree/Zipper"
, "Tree/Diff/Internal"
, "Tests"
]
|> stratifyWithPath
{ path = String.split "/"
, createMissingNode = String.join "/"
}
--> Ok (tree ""
--> [ tree "Tree"
--> [ singleton "Tree/Zipper"
--> , tree "Tree/Diff"
--> [ singleton "Tree/Diff/Internal"
--> ]
--> ]
--> , singleton "Tests"
--> ]
--> )
Because stratifyWithPath
creates missing nodes in the hierarchy,
it will succeed with most input data. The only way to make it fail
is if there are items with duplicate paths. In that case the Err
contains the path that was duplicated.
[ [ "foo" ] , [ "foo" ] ]
|> stratifyWithPath { path = identity, createMissingNode = identity }
--> Err [ "foo" ]
The root of the tree is always represented by the empty list. However, if the empty list has only one child, than the result will have that node as the root. This can avoid a problem of creating a superflous wrapper node.
restructure : (a -> b) -> (b -> List c -> c) -> Tree a -> c
Restructure a Tree
into another type of structure.
Imagine you have a Tree String
and you can to turn it into nested <ul>
s.
This function can help!
import Html exposing (Html)
labelToHtml : String -> Html msg
labelToHtml l =
Html.text l
toListItems : Html msg -> List (Html msg) -> Html msg
toListItems label children =
case children of
[] ->
Html.li [] [ label ]
_ ->
Html.li []
[ label
, Html.ul [] children
]
tree "root"
[ tree "folder"
[ singleton "foo"
, singleton "bar"
]
, singleton "yeah"
]
|> restructure labelToHtml toListItems
|> \root -> Html.ul [] [ root ]
--> Html.ul []
--> [ Html.li []
--> [ Html.text "root"
--> , Html.ul []
--> [ Html.li []
--> [ Html.text "folder"
--> , Html.ul []
--> [ Html.li [] [ Html.text "foo" ]
--> , Html.li [] [ Html.text "bar" ]
--> ]
--> ]
--> , Html.li [] [ Html.text "yeah" ]
--> ]
--> ]
--> ]
Or perhaps you have your own tree datastructure and you want to convert to it:
type MyTree a = MyTree a (List (MyTree a))
tree "root"
[ tree "folder"
[ singleton "foo"
, singleton "bar"
]
, singleton "yeah"
]
|> restructure identity MyTree
--> MyTree "root"
--> [ MyTree "folder"
--> [ MyTree "foo" []
--> , MyTree "bar" []
--> ]
--> , MyTree "yeah" []
--> ]
These functions have highly complex type signatures, but they abstract very generic ways of working with trees and in fact nearly all the other functions in this library are built using them. In general it is better to prefer the simpler interfaces, but there are situations that may not be covered by other functions in this library, where these more powerful functions can come in handy.
Note that all the callbacks passed receive four arguments:
state
variable that is accumulated throughout the whole computationancestors
, that is all the labels that lie above the current nodelabel
of the node being processedchildren
I like to call these \s a l c
, since "salc" is nice and pronouncable and quite
easy to remember.
Controls if the fold should continue traversing the tree, or should abort immediately.
breadthFirstFold : (s -> List a -> a -> List (Tree a) -> Step s) -> s -> Tree a -> s
Traverses the tree by "levels".
tree 1
[ tree 3
[ singleton 5
, singleton 4
]
, singleton 2
, singleton 6
]
|> breadthFirstFold (\s a l c -> Continue (l :: s)) []
--> [4, 5, 6, 2, 3, 1]
(The list is reversed here due to how ::
works)
depthFirstFold : (s -> List a -> a -> List (Tree a) -> Step s) -> s -> Tree a -> s
Traverses the tree by by going down as far as possible before trying out any siblings.
tree 1
[ tree 3
[ singleton 5
, singleton 4
]
, singleton 2
, singleton 6
]
|> depthFirstFold (\s a l c -> Continue (l :: s)) []
--> [6, 2, 4, 5, 3, 1]
(The list is reversed here due to how ::
works)