How to enforce shared behavior for subtype constructors of abstract type

In Makie I have an abstract type Layoutable. Whenever a Layoutable is created with a Figure passed as the parent, I want the layoutable added to the figure’s children. The problem is that this behavior needs to happen in the constructor, which is unique for each struct. So I have no location where I can place a generic function that allows me to hook behavior into every Layoutable subtype.

Right now I’m doing this with a macro, so one has to create new Layoutable types via macro and the behavior I want is written into the inner constructor. Is there no better way to do this?

Usually the logic would be

function outer(x::Abstract)
    # do generic stuff before specific implementation, then
    specific_stuff(x)
    # do generic stuff after specific implementation
end

The problem is that for construction there is no outer shared by all subtypes, as it’s already given by each struct’s name. People will write Button(...), Legend(...), etc.

1 Like

hm, this seems indeed hard to do in general without a macro,
since you definitely need to define a method for each type that dispatches to
your outer constructor instead of the normal one.

This is the simplest thing that I could come up with,
but of course that is brittle since it isn’t actually enforced.

struct Inner end

abstract type Layoutable end

struct Parent
    children
end


function Layoutable(::Type{T}, parent::Parent, args...; kwargs...) where T <: Layoutable
    # generic stuff

    child = T(Inner(), parent, args...; kwargs) # specific stuff

    push!(parent.children, child)
end


struct Button <: Layoutable
    x
    Button(::Inner, parent, x; kwargs...) = new(x)
end

Button(p::Parent, args...) = Layoutable(Button, p, args...)


p = Parent([])

b = Button(p, 42)

So the “agreement” would be to always forward the normal constructor to the abstract type first
and via dispatching on a singleton Inner type you could then call the inner constructor.

I’m not sure if this answers your question, but just from the point of view of dispatch you can do things like:

julia> abstract type Layoutable end

julia> struct Axis<:Layoutable
           label::String
           position::Float64
       end

julia> (::Type{T})(figure) where {T<:Layoutable} = T("x", 0.0)

julia> Axis(nothing)
Axis("x", 0.0)

This of course is a silly example, but I imagine something like

function (::Type{T})(figure) where {T<:Layoutable}
    # do generic stuff before specific implementation, then
    specific_stuff(T, figure)
    # do generic stuff after specific implementation
end

And then for every layoutable (e.g. Axis) you just overload specif_stuff(::Type{Axis}, figure).

2 Likes

ah I didn’t think of dispatching on the constructor like that, I’ll have to think about if that can solve my problem cleanly

This worked, my mistake was adding methods for the layoutables like Legend(somearg, otherarg...) directly, which doesn’t work together with a supertype intercept scheme like this. But changing all those methods to layoutable(Type, somearg, otherarg...) etc. and calling that from the abstract constructor works fine.

1 Like