I’m developing a tree-like structure where each node can have children of different types. Some of the types of nodes are limited in the types and number of the nodes shown below.
abstract type Component end
struct ComponentA <: Component
children::AbstractArray{Component,1}
data::Int
end
struct ComponentB <: Component
children::AbstractArray{Component,1}
name::AbstractString
end
struct ComponentC <: Component
child1::Component
child2::Component
end
Most of the different types will have just a simple array of children components, so I can define a simple fallback method,
children(x::Component) = x.children
However some like ComponentC above need a bit more specialisation. I’ve thought of two ways that I can do this
Define a new method for the types which require specialisation
Overload getproperty for those types so that the original children function works
function Base.getproperty(x::ComponentC, s::Symbol)
if s === :children
return Component[x.child1, x.child2]
else
return getfield(x, s)
end
end
I think I prefer 1 over 2, but I was wondering if there were any benefits either way. What are the pros and cons of each method? Is there a ‘correct’ way to do this?
Personally, I prefer the function approach, alternative 1.
The main difference is, I think, style, so it depends on what you prefer. Additionally, the property approach helps you with tab-completion, which is nice. But the function approach makes it more convenient to map over a collection, e.g.
children.(X)
# or
x |> foo |> children |> bar
instead of
getproperty.(X, :children)
# or
x |> foo |> (v -> getprop(v, :children)) |> bar
You can of course do both.
I have some more remarks:
For performance reasons, you should not use abstract types for the fields. Unlike in function signatures, this does actually have an impact. Either use just
struct ComponentB <: Component
children::Vector{Component}
name::String
end
or parameterize the type to allow different array and string types (but for simple lists of components and names, I doubt this is useful.)
Also, here
you are forcing the vector to have an abstract eltype, instead of just l allowing Julia to figure out the type automatically by writing
children(x::ComponentC) = [x.child1, x.child2] # no type specification
Yes, just do this. Generally, this is easier to extend, eg if you introduce a ComponentD that needs some other kind of specialization and lives in another package.
Unrelated, but if possible don’t use abstract types in fields. If you make them Vector{Component} and String rather than AbstractVector{Component} and AbstractString, Julia will generate code that is faster, more compact, and more resistant to invalidation. This is true even though Component itself is abstract; Vector{Component} is a concrete type even though the elements are not.