Stream: helpdesk (published)

Topic: Struct with factory


view this post on Zulip DrChainsaw (Oct 03 2021 at 13:48):

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.

view this post on Zulip Jeffrey Sarnoff (Oct 04 2021 at 17:24):

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"

view this post on Zulip DrChainsaw (Oct 05 2021 at 09:04):

@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 Ts 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.

view this post on Zulip Jeffrey Sarnoff (Oct 06 2021 at 05:01):

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
push!(gatherer.data, gatherable(mean, std))


Last updated: Oct 02 2023 at 04:34 UTC