What's the best practice to avoid forcing users into adding specific fields of their struct?

I’d like to user-friendly interface in my simulator.
So, I don’t wanna force users to add some specific struct fields.

However, I encountered some issues in my usage pattern.
For example, FymEnv is a type given by my package, and Env will be user-defined struct.
f (meaning update proceduer in simulator) will be the user-defined function for simulation. integral is provided by the package.
I’d like to record Env’s property (=env’s size, namely) once (which can be calculated at the beginning of simulation) and reuse it to enhance simulation performance.
Here is that looks like what I want:

struct Env <: FymEnv
    a  # something
end
function f(env::Env)
    a = env.a
    integral(env)  # how to refer its size inside `integral` without attaching field `size`?
    return a  # just an illustrative example
end

However, integral requires env’s size and calculate it at every instant may make the performance bad.
Of course, I may force users to add field size to their struct Env but it’s quite annoying to me because the envs. can be very large and nested.
On the other hand, if I provide another struct (FymSym, namely) as follows,

struct Sim <: FymSym
    env::Env
    size
end

users have to consider it and change the function f like

function f(sim::FymSym)
    a = sim.env.a  # annoying
    integral(sim)  # can get size by `sim.size` inside `integral`
    return a  # just an illustrative example
end

If there any good idea to solve it?

For more details, this issue is originally from avoiding exhausted calculation of the size of state, addressed in LazyFym.jl.

Why not simply have a function a (or a longer name if “a” is too short) that can be implemented for each env? This kind of getter pattern is both idiomatic and works around you having to know where a is located on each type.

1 Like

If you mean a(env::Env) = env.a, then I don’t understand how it solves this problem.

Can you give me a detailed example?

Not quite-- the whole idea is that you instruct your users to define the YourPkg.a() function for their particular type. Then you can write generic functions in terms of this a() function, and your code will work with any type which has defined a method for a(), regardless of its particular fields.

This is how nearly every interface in Julia works, so I’d suggest looking here for some real-life examples: Interfaces · The Julia Language

8 Likes

You’re right, defining a(env::Env) = env.a just to use a(env1) instead of env1.a seems redundant, but it gets really convenient for more complicated types. For Sim, you can define a(sim::Sim) = sim.env.a, so a(sim1) works. You can then feed both Env and Sim into the same method that calls a:

function f(x)
    y = a(x)
    # rest of code #
end

It even works on types with no a field. Let’s say you have a struct A1sauce where a hypothetical a value should always be 1. It’s a waste of memory to add an a field just to store a 1 there for every instance. Just define a(::A1sauce) = 1.

4 Likes

Or, maybe preferably, a(sim::Sim) = a(sim.env). Then the outer function doesn’t need to understand how env is structured.

5 Likes