Skip to content

Commit 781a01d

Browse files
authored
Merge pull request #120 from ExpandingMan/cursorfix
atonement for compiler abuse (type stable cursors!)
2 parents 7285f40 + 89f81bd commit 781a01d

10 files changed

+305
-96
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "AbstractTrees"
22
uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
33
authors = ["Keno Fischer <[email protected]>"]
4-
version = "0.4.2"
4+
version = "0.4.3"
55

66
[compat]
77
julia = "1"

docs/src/faq.md

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

+27
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

+1
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

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export nodetype, nodevalue, nodevalues, children, parentlinks, siblinglinks, chi
3434
#extended interface
3535
export nextsibling, prevsibling
3636

37+
export AbstractNode, StableNode
38+
3739
# properties
3840
export ischild, isroot, isroot, intree, isdescendant, treesize, treebreadth, treeheight, descendleft, getroot
3941

src/base.jl

+82
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+

0 commit comments

Comments
 (0)