Nested struct or something else?

In a certain program, I need to cater with a model M :=(omega, eos), where omega:=(omega_1,... omega_P) and eos:=(eos_1,..., eos_Q) are “lists” of P and Q real parameters, with P known in advance (at compile time), but Q only at run time. The whole model M or its “component lists”, omega or eos, by themselves, appear in several functions, either as explicit or implicit arguments.

In fact, I have, at least, two physically well-motivated situations where either Q is 2 or 3; thus I thought of defining Julia struct’s such that:

using Parameters

@with_kw struct Omega{T<;Real}
  omega_1 = 70.0
  ...
  omega_P = 0.7
end

@with_kw Eos1{T<:Real}
  w_0 = -1.0
  w_a = 0.0
end

@with_kw Eos2{T<:Real}
  A = 0.5
  z_f = -1.9
  z_t = 1.7
end

Eos = Union(Eos1, Eos2)

#and finally:

@with_kw struct ModelM
  omega::Omega
  eos::Eos
end

With these types, I hope to be able to easily define my functions such that there are not too many explicit called arguments and, at the same time, I am still able to use them conveniently for _integration, optimization, plotting, etc, on the parameters above (omega_1,…,omega_P, w_0, w_a, A, z_f, z_t), by choosing conveniently the fields of the different struct’s thus defined, via @unpack and/or Ref.
Question 1: Does this seem to be a good strategy?

Explicitly, sometimes I will have functions such as;

function distance(z, m::ModelM)
 #function body
end

Question 2: Should I explicitly indicate the type for m? For me, it seems to help legibility a bit, but I wonder about performance…

Some other times, my functions are just explicit functions of only eos::Eos, not of omega::Omega, such as:

function piezoenergeticratio(z, omega)
# function body
end

Finally,
Question 3: I wonder whether sometimes it would be cumbersome to access the nested struct fields and whether an altogether distinct approach would be wiser, declaring the functions with all explicit arguments (with variable number of them and somehow “flattened”). Is this the case?

Any pointers for documentation or packages, relevant corresponding comments or general coding advice are deeply appreciated!

That won’t affect performance. It might be useful to debug the code.

Concerning the general question, maybe this thread helps, particularly what I have marked as a solution to it, which is the possibility of writing function-like objects. The types of structures of objects and functions to achieve what you want are all there, I think: Define data-dependent function, various ways

2 Likes

@lmiq Thank you for your info about question 2 and general advice directing to another post here at Discourse; I will have to ponder over it. What about questions 1 and 3?

Your question 1 and 3 are kinda vague.

To know if a strategy is good or not someone needs to understand where it will be used, and if its weakpoints are not a problem given the circumstances. The way you used Eos and Omega in ModelM makes both fields of ModelM to be not-concrete. This has some performance penalties, but if this is not your bottleneck, then there is no problem. I just recommend to you that, when you access the fields, you pass them to a function that do the heavy work (instead of doing the heavy work directly in that function), so you do not have to deal with type unstability in the code that does the heavy work.

I do not think so. Your second scenario seems to be a scope nightmare. Just use @unpack for easy access (from the package with the same name if I am not wrong).

1 Like

As Henrique mentioned, the other questions are somewhat vague. But I have made some changes to the code above to improve the syntax.

Some small things:

You probably want just

@with_kw Eos1
  w_0::Float64 = -1.0 
  w_a::Float64 = 0.0
end

(not T<:Real). You can initialize Eos1 with an integer anyway, and that will be more transparent and less error prone in terms of type stability.

Concerning the general question, the most common answer to it is the use of closures. Using a struct to carry the data is fine. But you will find many cases in Julia like:

julia> inner(f,x) = f(x) 

julia> function outer(f,x)
         inner(f,x)
       end
outer (generic function with 1 method)

julia> g(x,data) = data*x
g (generic function with 1 method)

julia> const mydata = 5.
5.0

julia> x = 10.
10.0

julia> outer(x -> g(x,mydata), x)
50.0

As you can see, the “multiple parameters” represented here by mydata can be fed only to the anonymous function x->g(x,mydata) which is an argument of the outer function, and what is passed along is the function itself.