Specialising behaviour of subtypes; methods or properties?

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

  1. Define a new method for the types which require specialisation

    children(x::ComponentC) = Component[x.child1, x.child2]
    
  2. 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
4 Likes

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.

2 Likes

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.

4 Likes

Wonderful, thanks for the help