Best practice for storing functions in a struct

Just thought you might be interested to know: I’ve seen plenty of your and Steven’s comments on here before, and always thought highly of them. So I took a day off and came back with fresh eyes and now agree with you that passing functions around in structs was going to be a mistake. I’ve re-drafted the framework using dedicated transformation types and have come up with something that I think is a Pareto improvement in flexibility on what I had before (and now uses Julia’s dispatch system for everything). In summary, many thanks. You may have saved me some real grief down-the-line.

Cheers,

Colin

5 Likes

Hello,

I don’t know if it is ok to revive this topic or not, but I have the same question and any of the posts respond to my needs :confused: .

So, I’m doing a numerical solver for a certain type of PDE. The PDE in study accepts parameters and 3 functions as inputs.
Since, I’m wrapping my inputs inside a structure to pass to a function. So, I have 3 fields in my structure reserved for the functions.

But if I define my structure as:

struct Input{T<:Signed, P<:AbstractFloat,F<:Function}
    a::T
    b::P
    I::F
    K::F
    S::F
end

This give me the following error:

I(x) = x^2
K(x) = exp(-x)
S(V) = tanh(V)

julia> input = Input(3,4.5,I,K,S)
ERROR: MethodError: no method matching Input(::Int64, ::Float64, ::typeof(I), ::typeof(K), ::typeof(S))
Closest candidates are:
  Input(::T, ::P, ::F, ::F, ::F) where {T<:Signed, P<:AbstractFloat, F<:Function} at REPL[51]:2
Stacktrace:
 [1] top-level scope at REPL[55]:1

But, instead if I define my structure like this:

struct Input{T<:Signed, P<:AbstractFloat}
    a::T
    b::P
    I::Function
    K::Function
    S::Function
end

Everything works fine, but I read somewhere not to do I::Function inside of a structure. Any help?

Once again, sorry for digging up such an old post

2 Likes

Better to make a new topic for this sort of thing–can one of the moderators split this off?

In your case, the problem is that different functions in Julia have distinct types, so you can’t have fields I K and S of the same type. One easy (if rather verbose) solution is to have F_I, F_K and F_S as separate type parameters.

BTW, it’s OK to have {F <: Function} in your type parameters, but you might want to consider just having {F} instead. There are function-like objects in Julia (like a callable struct) which are not subtypes of Function, so you may not want to prevent users from using such structs.

That would look like:

struct Input{T <: Signed, P <: AbstractFloat, F_I, F_K, F_S}
  a::T 
  b::P  
  I::F_I
  K::F_K
  S::F_S
end
8 Likes

Thank you so much, that explained everything to me! I will use that approach to what I have in hands, since I will always have only 3 functions as inputs, i think it’s ok to write the struct like that.

Thanks!

So I have a problem that takes this discussion to the next level. I’m running a simulation on a plant, and I need to define an arbitrary set of instruments. These instruments have an observed value, but also need to execute a piece of code to obtain the measurement.

mutable struct Instrument{F}
   ID :: Symbol
   value :: Float64
   expression :: F
end

inst = Instrument( :Stream1_VolumetricFlow, NaN64,  
         x-> get_mass_flow(x[:Stream1])/get_density(x[:Stream1]) )

Each instrument has its own custom line of code as it has to do something to different parts of the system (sometimes it’s literally just getting a field, like Stream1.temperature). Now, in order to get the measurement errors, I need to iterate over all of the instruments. I think I have a few options:

  1. Build a tuple of Instruments (having different “function” parameters) and iterate over that (I don’t see much of a downside here, but that is a lot of different functions)
  2. Build an array of Instruments (note that the parameter types are different, I’m not sure if Arrays can handle this efficiently) and iterate over that
  3. Make the Instrument type non-parameteric, and have “expression :: Function”. I’m told this is an abstract type and that this will mean the compiler is not optimized. Nevertheless it makes the array quite uniform.
  4. Build a generated function that uses the tuple of Instruments as input, (this will put all the code in one place as though it written inline, but there’s hundreds of instruments so this looks like it will be one heck of a type signature; a tuple of Instruments with hundreds of type parameters I’m not sure if there’s a significant performance penalty for that).

There’s so many options, I’m not sure which one I should do.

Which fields in Instrument are you mutating?

Only the “value”

Let’s say there are three instruments, 1, 2, and 3. Is the expression for instrument 1 always the same? As in, if I go away and come back in a few days will the expression for instrument 1 still be the same expression?

BTW what I’m thinking is that it might be better to split the mutable bit (the Float64) out into something else so that Instrument doesn’t need to be mutable anymore.

Also is the Symbol unique to each Instrument?

Bit of a drastic change, but could you do this:

#Structural framework
struct Instrument1 ; end
struct Instrument2 ; end
struct Instrument3 ; end

#The expression field is now split out here into dedicated functions leveraging multiple dispatch
#Note, expression_func can have additional input fields if needed, such as the dict below
expression_func(::Type{Instrument1}) = some transform1
expression_func(::Type{Instrument2}) = some transform2
expression_func(::Type{Instrument3}) = some transform3

#Similarly we map instrument to id using dedicated functions and multiple dispatch
instrument_id(::Type{Instrument1}) = :id1
instrument_id(::Type{Instrument2}) = :id2
instrument_id(::Type{Instrument3}) = :id3

#Instruments are linked to their values in a dictionary which will be very fast since it just maps DataType to Float64
#Dictionary values can be changed at runtime very efficiently and can be looped over very fast
d = Dict{DataType,Float64}()
d[Instrument1] = NaN
d[Instrument2] = NaN
d[Instrument3] = NaN

Note, if expression_func needs to mutate the values, then we would do something like:

expression_func!(::Type{Instrument1}, d::Dict{DataType,Float64}) = some transform1
expression_func!(::Type{Instrument2}, d::Dict{DataType,Float64}) = some transform2
expression_func!(::Type{Instrument3}, d::Dict{DataType,Float64}) = some transform3

If your framework fits in this system, then it will be very fast.

Yes, both the ID and the instrument are the same. If either change, it’s a different instrument.

You may want to move this to a new thread - this is drifting from the original question.
In practice, the question of “how uncertain is the measurement coming from this instrument?” requires a ton of business logic, and you’ll be better served by a type hierarchy with concrete types for each individual instrument.

Instead of a mutable ‘value’ tied to the instrument, you can write a bunch of methods like measure(container, instrument, measurement) with the argument types tied into your type hierarchy

abstract type Instrument end
abstract type FlowMeter <: Instrument end
abstract type ThermalFlowMeter <: FlowMeter end
abstract type CoriolisFlowMeter <: FlowMeter end

struct AlicatMFM <: ThermalFlowMeter
    min::Float64
    max::Float64
    response_time::Float64
    reading_accuracy::Float64
    fullscale_accuracy::Float64
end

abstract type Fluid end

struct IdealGas <: Fluid
    molar_mass::Float64
    temperature::Float64
end

T(g::IdealGas) = g.temperature
# ρ(g::IdealGas) = ...
# p(g::IdealGas) = ...

abstract type Pipe end

struct SteelPipe{T<:Fluid} <: Pipe
   diameter::Float64
   roughness::Float64
   massflow::Float64
   contents::T
end

function massflow(p::Pipe, m::ThermalFlowMeter)
    #...deal with dead band, out-of-range, rdg/full-scale accuracy, ...
    return measurement ± err
end
2 Likes