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 Component
s. 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 Component
s that hold a composition of Component
s 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 Component
s, 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 Component
s with trait Composite{N}
, hold a tuple of Component
s so all type information should be resolved at compile-time.
Apologies in advance for the longest MWE you will have seen… Thanks!