Skip to content

Commit 8398e89

Browse files
committed
added AbstractNode, StableNode, and a bunch of docs
1 parent 8c2c32a commit 8398e89

File tree

8 files changed

+148
-39
lines changed

8 files changed

+148
-39
lines changed

docs/src/faq.md

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,29 +66,3 @@ map leaves.
6666

6767
Introducing a new type becomes necessary to ensure that it can accommodate arbitrary output types.
6868

69-
# Why is my code type unstable?
70-
Guaranteeing type stability when iterating over trees is challenging to say the least. There are
71-
several major obstacles
72-
- The children of a tree node do not, in general, have the same type as their parent.
73-
- Even if it is easy to infer the type of a node's immediate children, it is usually much harder to
74-
infer the types of the node's more distant descendants.
75-
- Navigating a tree requires inferring not just the types of the children but the types of the
76-
children's *iteration states*. To make matters worse, Julia's `Base` does not include traits
77-
for describing these, and the `Base` iteration protocol makes very few assumptions about them.
78-
79-
All of this means that you are unlikely to get type-stable code from AbstractTrees.jl without some
80-
effort.
81-
82-
The simplest way around this is to define the `NodeType` trait and `nodetype` (analogous to
83-
`Base.IteratorEltype` and `eltype`):
84-
```julia
85-
AbstractTrees.NodeType(::Type{<:ExampleNode}) = HasNodeType()
86-
AbstractTrees.nodetype(::Type{<:ExampleNode}) = ExampleNode
87-
```
88-
which is equivalent to asserting that all nodes of a tree are of the same type. Performance
89-
critical code must ensure that it is possible to construct such a tree, which may not be trivial.
90-
91-
Note that even after defining `Base.eltype` it might still be difficult to achieve type-stability
92-
due to the aforementioned difficulties with iteration states. The most reliable around this is to
93-
ensure that the object returned by `children` is indexable and that the node has the
94-
`IndexedChildren` state. This guarantees that `Int` can always be used as an iteration state.

docs/src/index.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,33 @@ prevsiblingindex
145145
rootindex
146146
```
147147

148+
## The `AbstractNode` Type
149+
It is not required that objects implementing the AbstractTrees.jl interface are of this type, but it
150+
can be used to indicate that an object *must* implement the interface.
151+
```@docs
152+
AbstractNode
153+
```
154+
155+
## Type Stability and Performance
156+
Because of the recursive nature of trees it can be quite challenging to achieve type stability when
157+
traversing it in any way such as iterating over nodes. Only trees which guarantee that all nodes
158+
are of the same type (with [`HasNodeType`](@ref)) can be type stable.
159+
160+
To make it easier to convert trees with non-uniform node types this package provides the
161+
`StableNode` type.
162+
```@docs
163+
StableNode
164+
```
165+
166+
To achieve the same performance with custom node types be sure to define at least
167+
```julia
168+
AbstractTrees.NodeType(::Type{<:ExampleNode}) = HasNodeType()
169+
AbstractTrees.nodetype(::Type{<:ExampleNode}) = ExampleNode
170+
```
171+
172+
In some circumstances it is also more efficient for nodes to have [`ChildIndexing`](@ref) since this
173+
also guarantees the type of the iteration state of the iterator returned by `children`.
174+
148175
## Additional Functions
149176
```@docs
150177
getdescendant

docs/src/iteration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ PostOrderDFS
3434
Leaves
3535
Siblings
3636
StatelessBFS
37+
treemap
3738
```
3839

3940
### Iterator States

src/AbstractTrees.jl

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ include("iteration.jl")
1919
include("builtins.jl")
2020
include("printing.jl")
2121

22-
# Julia 1.0 support (delete when we no longer support it)
23-
if !isdefined(Base, :isnothing)
24-
isnothing(x) = x === nothing
25-
end
26-
2722

2823
#interface
2924
export ParentLinks, StoredParents, ImplicitParents
@@ -34,6 +29,8 @@ export nodetype, nodevalue, nodevalues, children, parentlinks, siblinglinks, chi
3429
#extended interface
3530
export nextsibling, prevsibling
3631

32+
export AbstractNode, StableNode
33+
3734
# properties
3835
export ischild, isroot, isroot, intree, isdescendant, treesize, treebreadth, treeheight, descendleft, getroot
3936

src/base.jl

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,85 @@ function getroot(::StoredParents, node)
191191
p = parent(p)
192192
end
193193
end
194+
195+
196+
"""
197+
AbstractNode{T}
198+
199+
Abstract type of tree nodes that implement the AbstractTrees.jl interface.
200+
201+
It is *NOT* necessary for tree nodes to inherit from this type to implement the AbstractTrees.jl interface.
202+
Conversely, all `AbstractNode` types are required to satisfy the AbstractTrees.jl interface (i.e. they must
203+
at least define [`children`](@ref)).
204+
205+
Package developers should keep in mind when writing methods that most trees *will not* be of this type.
206+
Therefore, any functions which are intended to work on any tree should not dispatch on `AbstractNode`.
207+
208+
The type parameter `T` is the type of the [`nodevalue`](@ref) of the concrete type descented from `AbstractNode`.
209+
"""
210+
abstract type AbstractNode{T} end
211+
212+
function Base.show(io::IO, node::AbstractNode)
213+
print(io, typeof(node), "(")
214+
show(io, nodevalue(node))
215+
print(io, ", nchildren=", length(children(node)), ")")
216+
end
217+
218+
Base.show(io::IO, ::MIME"text/plain", node::AbstractNode) = print_tree(io, node)
219+
220+
221+
"""
222+
StableNode{T} <: AbstractNode{T}
223+
224+
A node belonging to a tree in which all nodes are of type `StableNode{T}`. This type is provided so that
225+
trees with [`NodeTypeUnknown`](@ref) can implement methods to be converted to type-stable trees with indexable
226+
`children` which allow for efficient traversal and iteration.
227+
228+
## Constructors
229+
```julia
230+
StableNode{T}(x::T, ch)
231+
StableNode(x, ch=())
232+
StableNode(𝒻, T, node)
233+
```
234+
235+
## Arguments
236+
- `x`: the value of the constructed node, returned by [`nodevalue`](@ref).
237+
- `ch`: the children of the node, each must be of type `StableNode`.
238+
- `𝒻`: A function which, when called on the node of a tree returns a value which should be wrapped
239+
by a `StableNode`. The return value of `𝒻` must be convertable to `T` (see example).
240+
- `T`: The value type of the `StableNode`s in a tree.
241+
- `node`: A node from a tree which is to be used to construct the `StableNode` tree.
242+
243+
## Examples
244+
```julia
245+
t = [1, [2,3]]
246+
247+
node = StableNode(Union{Int,Nothing}, t) do n
248+
n isa Integer ? convert(Int, n) : nothing
249+
end
250+
```
251+
In the above example `node` is a tree with [`HasNodeType`](@ref), nodes of type `StableNode{Union{Int,Nothing}}`.
252+
The nodes in the new tree corresponding to arrays have value `nothing` while other nodes have their
253+
corresponding `Int` value.
254+
"""
255+
struct StableNode{T} <: AbstractNode{T}
256+
value::T
257+
children::Vector{StableNode{T}}
258+
259+
# this ensures proper handling of all cases for iterables ch
260+
StableNode{T}(x::T, ch) where {T} = new{T}(x, collect(StableNode{T}, ch))
261+
end
262+
263+
nodevalue(n::StableNode) = n.value
264+
265+
children(n::StableNode) = n.children
266+
267+
NodeType(::Type{<:StableNode}) = HasNodeType()
268+
nodetype(::Type{StableNode{T}}) where {T} = StableNode{T}
269+
270+
ChildIndexing(::Type{<:StableNode}) = IndexedChildren()
271+
272+
function StableNode{T}(𝒻, node) where {T}
273+
StableNode{T}(convert(T, 𝒻(node)), map(n -> StableNode{T}(𝒻, n), children(node)))
274+
end
275+

src/cursors.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ nodevalue(tc::TreeCursor) = tc.node
8181
parent(tc::TreeCursor) = tc.parent
8282

8383

84+
#====================================================================================================
85+
Note for developers:
86+
87+
The following code for `TreeCursor` types contains a fair amount of code duplication.
88+
In particular, some of the cursors can probably be combined (e.g. ImplicitCursor and StableCursor).
89+
90+
This duplication is deliberate: cursors can get very subtle and it is rather easy to break them,
91+
break their type stability or cause O(tree_depth) recursive compilation costs.
92+
====================================================================================================#
93+
94+
8495
"""
8596
TrivialCursor{N,P} <: TreeCursor{N,P}
8697

src/iteration.jl

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ Every `MapNode` is itself a tree with the [`IndexedChildren`](@ref) trait and th
430430
431431
Use [`AbstractTrees.nodevalue`](@ref) or `mapnode.value` to obtain the wrapped value.
432432
"""
433-
struct MapNode{T,C}
433+
struct MapNode{T,C} <: AbstractNode{T}
434434
value::T
435435
children::C
436436

@@ -450,13 +450,6 @@ nodevalue(μ::MapNode) = μ.value
450450

451451
ChildIndexing(::MapNode) = IndexedChildren()
452452

453-
function Base.show(io::IO, μ::MapNode)
454-
print(io, typeof(μ))
455-
print(io, "(", μ.value, ")")
456-
end
457-
458-
Base.show(io::IO, ::MIME"text/plain", μ::MapNode) = print_tree(io, μ)
459-
460453

461454
"""
462455
treemap(f, node)
@@ -479,6 +472,8 @@ It's very easy to write an `f` that makes `treemap` stack-overflow. To avoid th
479472
termiantes, i.e. that sometimes it returns empty `children`. For example, if `f(n) = (nothing, [0; children(n)])` will
480473
stack-overflow because every node will have at least 1 child.
481474
475+
To create a tree with [`HasNodeType`](@ref) which enables efficient iteration, see [`StableNode`](@ref) instead.
476+
482477
## Examples
483478
```julia
484479
julia> t = [1, [2, 3]];

test/builtins.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,25 @@ end
8686
@test nodevalue.(PostOrderDFS(b)) == [0, 1, 0, 2, 0, 3, nothing, nothing, nothing]
8787
end
8888

89+
@testset "StableNode" begin
90+
t = [1,[2,3,[4,5]]]
91+
92+
n = StableNode{Union{Int,Nothing}}(t) do m
93+
m isa Integer ? convert(Int, m) : nothing
94+
end
95+
96+
@test treeheight(n) == 3
97+
@test treebreadth(n) == 5
98+
99+
@test typeof(TreeCursor(n)) == AbstractTrees.StableIndexedCursor{StableNode{Union{Int,Nothing}}}
100+
101+
ls = @inferred collect(Leaves(n))
102+
ls = nodevalue.(ls)
103+
@test eltype(ls) <: Union{Nothing,Int}
104+
@test nodevalue.(ls) == 1:5
105+
106+
ns = @inferred collect(PreOrderDFS(n))
107+
ns = nodevalue.(ns)
108+
@test eltype(ns) == Union{Nothing,Int}
109+
@test ns == [nothing, 1, nothing, 2, 3, nothing, 4, 5]
110+
end

0 commit comments

Comments
 (0)