Is there some canonical pattern for parametric structs which need to dynamically create new instances of some subcomponent?
Here is a simple groupby-ish example with some alternatives (in order of generalizability) I can think of:
struct CollectorA{T}
data::Dict{Int, T}
end
function addsample_a!(c::CollectorA{T}, sample) where T
container = get!(c.data, id(sample)) do
T()
end
append!(container, sample)
end
function addsample_b!(c::CollectorA{T}, sample) where T
container = get!(c.data, id(sample)) do
make_new(T)
end
append!(container, sample)
end
struct CollectorB{T, F}
data::Dict{Int, T}
factory::F
end
function addsample_c!(c::CollectorB{T}, sample) where T
container = get!(c.data, id(sample)) do
c.factory()
end
append!(container, sample)
end
Any big reason to pick one over the other? In the real use case T
will only make sense if it is one out of a few types.
If your subcomponent is a Dict or an Array or other mutable entity, just identify it as the named field of your struct and update it as you would if it were not part of struct.
struct MyDict{T}
data::Dict{Int, T}
end
MyDict(::Type{T}) where {T} = MyDict(Dict{Int,T}())
mydict = MyDict(String)
mydict.data[1] = "one"
@Jeffrey Sarnoff Thanks, your example looks a bit likeaddsample_b!
with MyDict
having the role of make_new
but I'm not sure I understand it correctly.
In case more context is needed, the T
s in my original example could e.g. be something which stores each individual sample or some onlinestats if the user wants to use less memory.
Let's get more abstract for a moment. You have a structured type that is purposeful; it exists to fulfill the role of gatherer_of_items. You receive items of some shared kind, for example, the items may be sampled observations, or the items may be statistical summaries taken over many sequentially sampled observations, or something else. The important characteristic is the sameness of item's Type; while the nature of an item may be very different with different applications for your code, for any specific something to be gathered there is a specific type of item to gather.
[Yes, that specific type could itself be composite or have subsidiary components .. that does not change the design approach. Even where subsidiary functions are used, they are subsidiary to some parent functionality that is already part of the design. ]
struct GathererOfItems{T}
data::Vector{T}
end
# struct's constructor
GathererOfItems(::Type{T}) where {T} = GathererOfItems{T}(Vector{T}())
# realize of the struct (concrete instantiation)
ItemType = Float64 # could be a NamedTuple of statistics, a webpage, ...
gatherer = GathererOfItems(ItemType)
isempty(gatherer.data)
datum = ItemType(1.0)
push!(gatherer.data, datum)
datum = ItemType(2.0)
push!(gatherer.data, datum)
gatherer.data == [1.0, 2.0]
You can add each item just by using whatever means exists to absorb it.
Here, that is function push!
What if there is some creation process involving the item necessary before absorbing it?
Well, usually that construction will have occurred "already" (some time before the item is to be gathered). For example, if your samples are each of two values, a local mean and a stddev from the current subsequence of observations (μ, σ)
, and your gatherer expects a NamedTuple (mean = μ, std = σ)
, you may be better off with an intermediation function than by making a function to addsample
.
function gatherable(mean, std)
(; mean, std)
end
#
julia> gatherable( 10.0, 0.25)
(mean = 10.0., std = 0.25)
and use this
local_mean, local_std = observation()
push!(gatherer.data, gatherable(local_mean, local_std))
Last updated: Nov 06 2024 at 04:40 UTC