Something that’s come up repeatedly for me this last week in different contexts (and also many times before) is how do you approach the case where you sometimes want to be able to store intermediary results?
For a clearly contrived MWE, let’s say we have a type wrapping an Array and in certain contexts we often evaluate the extrema. Let’s say we don’t have to worry about cache invalidation (e.g. either 1) the values of the array don’t change or 2) we can only change the value using a setter function that flushes the cache). In a real setting the function (here extrema
) might be expensive.
I see a number of design patterns. The most obvious are to never cache or always cache (not what I’m after):
# 1. just the type
struct Mytype
v::Vector
end
lims(m::Mytype) = extrema(m.v)
# 2. type and cache
struct Mytype{T}
v::Vector{T}
lims::Tuple(T, T)
Mytype(a::AbstractVector{T}) where T = new{T}(a, extrema(a))
end
lims(m::Mytype) = m.lims
But what if I want to cache the object? I see two possibilities. Either I cache it the first time it is called:
# 3. Cache at first request
struct Mytype{T}
v::Vector{T}
lims::Ref{Union{Nothing, Tuple{T,T}}}
Mytype(v::AbstractVector{T}) where T = new{T}(v, nothing)
end
function lims(m::Mytype) #not `lims!` I think as the mutation doesn't change behaviour
isnothing(m.lims[]) && (m.lims[] = extrema(m.v))
m.lims[]
end
Or have a special type that one can invoke at will, giving full control to the user but at the cost of increased coding complexity for little gain:
# 4. Different types to signify cache
abstract type Myabstracttype end
struct Mytype <: Myabstracttype
v::Vector
end
struct Mytype_cached{T} <: Myabstracttype
v::Vector{T}
lims::Tuple(T, T)
end
lims(m::Mytype) = extrema(m.v)
lims(m::Mytype_cached) = m.lims
cachelims_mytype(m::Mytype) = Mytype_cached{eltype(m.v)}(v, lims(v))
I can see pros and cons of all (as outlined above) and I do realize that coding style is a preference, but:
- Which of these design patterns are preferred?
- Is this even an ideomatic thing to do in Julia?