I am simulating electronic circuits and I have chosen to adopt a modular model which works on objects of type Component. A Component can be a Resistor, Capacitor, or even a Serial/Parallel combination of Components. This is reflected in the following code block:
abstract type Component end
mutable struct Resistor <: Component end
mutable struct Capacitor <: Component end
struct Serial{P} <:Component
components::P
Serial(v::Vararg{<:Component,N}) where N = new{typeof(v)}(v)
end
struct Parallel{P} <:Component
components::P
Parallel(v::Vararg{<:Component,N}) where N = new{typeof(v)}(v)
end
I would like to apply a generic function (applyf) to a Component, but I would like to distinguish between Components that hold a composition of Components and a singular Component. To achieve this, I have adopted the `Holy Traits’ technique:
abstract type CompositionTrait end
struct Leaf <: CompositionTrait end
struct Composite{N} <: CompositionTrait end
CompositionTrait(::Type{<:Resistor}) = Leaf()
CompositionTrait(::Type{<:Capacitor}) = Leaf()
CompositionTrait(::Type{<:Serial{P}}) where P = Composite{length(P.parameters)}()
CompositionTrait(::Type{<:Parallel{P}}) where P = Composite{length(P.parameters)}()
Great! I can now write different functions for a Component which has the trait Composite{N} and a Component which has the trait Leaf.
applyf(c::Component) = applyf(CompositionTrait(typeof(c)), c)
applyf(::Leaf, c::Component) = nothing
function applyf(::Composite{N}, c::Component) where N
for i = 1:N
applyf(c.components[i])
end
nothing
end
This works as expected. Moreover, for simple compositions of Components, the code is very efficient with zero allocations. However, the following complicated (nested compositions) component causes allocations:
using BenchmarkTools
const N = 10
component =
Serial(
(Parallel(
(Serial(Resistor(),Capacitor()) for _=1:N)...
) for i=1:N
)...
)
>julia @btime applyf($component)
'316.843 ns (10 allocations: 1.72 KiB)'
When starting Julia with --track-allocation=user, it notifies me that the trait dispatch line applyf(c::Component), is causing the allocations. Why is this? The Components with trait Composite{N}, hold a tuple of Components so all type information should be resolved at compile-time.
Apologies in advance for the longest MWE you will have seen… Thanks!