Skip to content

Commit

Permalink
Merge pull request #120 from ExpandingMan/cursorfix
Browse files Browse the repository at this point in the history
atonement for compiler abuse (type stable cursors!)
  • Loading branch information
oscardssmith authored Oct 19, 2022
2 parents 7285f40 + 89f81bd commit 781a01d
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 96 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "AbstractTrees"
uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
authors = ["Keno Fischer <[email protected]>"]
version = "0.4.2"
version = "0.4.3"

[compat]
julia = "1"
Expand Down
26 changes: 0 additions & 26 deletions docs/src/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,29 +66,3 @@ map leaves.

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

# Why is my code type unstable?
Guaranteeing type stability when iterating over trees is challenging to say the least. There are
several major obstacles
- The children of a tree node do not, in general, have the same type as their parent.
- Even if it is easy to infer the type of a node's immediate children, it is usually much harder to
infer the types of the node's more distant descendants.
- Navigating a tree requires inferring not just the types of the children but the types of the
children's *iteration states*. To make matters worse, Julia's `Base` does not include traits
for describing these, and the `Base` iteration protocol makes very few assumptions about them.

All of this means that you are unlikely to get type-stable code from AbstractTrees.jl without some
effort.

The simplest way around this is to define the `NodeType` trait and `nodetype` (analogous to
`Base.IteratorEltype` and `eltype`):
```julia
AbstractTrees.NodeType(::Type{<:ExampleNode}) = HasNodeType()
AbstractTrees.nodetype(::Type{<:ExampleNode}) = ExampleNode
```
which is equivalent to asserting that all nodes of a tree are of the same type. Performance
critical code must ensure that it is possible to construct such a tree, which may not be trivial.

Note that even after defining `Base.eltype` it might still be difficult to achieve type-stability
due to the aforementioned difficulties with iteration states. The most reliable around this is to
ensure that the object returned by `children` is indexable and that the node has the
`IndexedChildren` state. This guarantees that `Int` can always be used as an iteration state.
27 changes: 27 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,33 @@ prevsiblingindex
rootindex
```

## The `AbstractNode` Type
It is not required that objects implementing the AbstractTrees.jl interface are of this type, but it
can be used to indicate that an object *must* implement the interface.
```@docs
AbstractNode
```

## Type Stability and Performance
Because of the recursive nature of trees it can be quite challenging to achieve type stability when
traversing it in any way such as iterating over nodes. Only trees which guarantee that all nodes
are of the same type (with [`HasNodeType`](@ref)) can be type stable.

To make it easier to convert trees with non-uniform node types this package provides the
`StableNode` type.
```@docs
StableNode
```

To achieve the same performance with custom node types be sure to define at least
```julia
AbstractTrees.NodeType(::Type{<:ExampleNode}) = HasNodeType()
AbstractTrees.nodetype(::Type{<:ExampleNode}) = ExampleNode
```

In some circumstances it is also more efficient for nodes to have [`ChildIndexing`](@ref) since this
also guarantees the type of the iteration state of the iterator returned by `children`.

## Additional Functions
```@docs
getdescendant
Expand Down
1 change: 1 addition & 0 deletions docs/src/iteration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ PostOrderDFS
Leaves
Siblings
StatelessBFS
treemap
```

### Iterator States
Expand Down
2 changes: 2 additions & 0 deletions src/AbstractTrees.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export nodetype, nodevalue, nodevalues, children, parentlinks, siblinglinks, chi
#extended interface
export nextsibling, prevsibling

export AbstractNode, StableNode

# properties
export ischild, isroot, isroot, intree, isdescendant, treesize, treebreadth, treeheight, descendleft, getroot

Expand Down
82 changes: 82 additions & 0 deletions src/base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,85 @@ function getroot(::StoredParents, node)
p = parent(p)
end
end


"""
AbstractNode{T}
Abstract type of tree nodes that implement the AbstractTrees.jl interface.
It is *NOT* necessary for tree nodes to inherit from this type to implement the AbstractTrees.jl interface.
Conversely, all `AbstractNode` types are required to satisfy the AbstractTrees.jl interface (i.e. they must
at least define [`children`](@ref)).
Package developers should keep in mind when writing methods that most trees *will not* be of this type.
Therefore, any functions which are intended to work on any tree should not dispatch on `AbstractNode`.
The type parameter `T` is the type of the [`nodevalue`](@ref) of the concrete type descented from `AbstractNode`.
"""
abstract type AbstractNode{T} end

function Base.show(io::IO, node::AbstractNode)
print(io, typeof(node), "(")
show(io, nodevalue(node))
print(io, ", nchildren=", length(children(node)), ")")
end

Base.show(io::IO, ::MIME"text/plain", node::AbstractNode) = print_tree(io, node)


"""
StableNode{T} <: AbstractNode{T}
A node belonging to a tree in which all nodes are of type `StableNode{T}`. This type is provided so that
trees with [`NodeTypeUnknown`](@ref) can implement methods to be converted to type-stable trees with indexable
`children` which allow for efficient traversal and iteration.
## Constructors
```julia
StableNode{T}(x::T, ch)
StableNode(x, ch=())
StableNode(𝒻, T, node)
```
## Arguments
- `x`: the value of the constructed node, returned by [`nodevalue`](@ref).
- `ch`: the children of the node, each must be of type `StableNode`.
- `𝒻`: A function which, when called on the node of a tree returns a value which should be wrapped
by a `StableNode`. The return value of `𝒻` must be convertable to `T` (see example).
- `T`: The value type of the `StableNode`s in a tree.
- `node`: A node from a tree which is to be used to construct the `StableNode` tree.
## Examples
```julia
t = [1, [2,3]]
node = StableNode(Union{Int,Nothing}, t) do n
n isa Integer ? convert(Int, n) : nothing
end
```
In the above example `node` is a tree with [`HasNodeType`](@ref), nodes of type `StableNode{Union{Int,Nothing}}`.
The nodes in the new tree corresponding to arrays have value `nothing` while other nodes have their
corresponding `Int` value.
"""
struct StableNode{T} <: AbstractNode{T}
value::T
children::Vector{StableNode{T}}

# this ensures proper handling of all cases for iterables ch
StableNode{T}(x::T, ch) where {T} = new{T}(x, collect(StableNode{T}, ch))
end

nodevalue(n::StableNode) = n.value

children(n::StableNode) = n.children

NodeType(::Type{<:StableNode}) = HasNodeType()
nodetype(::Type{StableNode{T}}) where {T} = StableNode{T}

ChildIndexing(::Type{<:StableNode}) = IndexedChildren()

function StableNode{T}(𝒻, node) where {T}
StableNode{T}(convert(T, 𝒻(node)), map(n -> StableNode{T}(𝒻, n), children(node)))
end

Loading

2 comments on commit 781a01d

@oscardssmith
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/70619

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.4.3 -m "<description of version>" 781a01d54b0441dfde6c911274480b7345323a69
git push origin v0.4.3

Please sign in to comment.