Access field of custom struct by user-provided name

Suppose I want a struct that has to store a variable which can be quite general. Most typically, it will contain an array of forces, or alternatively an energy value. Something like:

struct System{T}
    property::T
end

The user initializes the system with:

forces = zeros(10)
system = System(forces)

That is fine, and now he/she can access the forces using…

system.property

Is there a way to overload getproperty in such a way that the user is able to retrieve the forces
using

system.forces

?

Note that he/she could also have started with:

energy = 0.0
system = System(energy)

And now I would like that system.energy retrieved the system.property value, as well.

(ps: If I need a macro for this - which obviously can do the trick, let it go, I prefer to avoid macros).

Edit:

The best I could do so far is to define an additinal field property_name, and use something like:

julia> struct System{T}
           property_name::Symbol
           property::T
       end

julia> function Base.getproperty(s::System, a::Symbol) 
           if a == getfield(s, :property_name) 
               return getfield(s, :property)
           else
               return getfield(s, a)
           end
       end

julia> s = System(:energy, 0.0)
System{Float64}(:energy, 0.0)

julia> s.energy
0.0

Not too bad, but somewhat redundant. I guess I won’t be able to do that better without a macro, as no function will be able to access the name of the incoming variable.

Without a macro I don’t think you can get the name of a variable that was passed into a function. However, you could do something like this:

struct System{T}
    propertyName::Symbol
    property::T
end

system1 = System(:forces, zeros(10))
system2 = System(:energy, 0.0)

#Set up a custom getproperty
Base.getproperty(sys::System, f::Symbol) = isequal(f, getfield(sys, :propertyName)) ? getfield(sys, :property) : error("type System has no field $(f)")

#These works
system1.forces
system2.energy

#But these do not work
system1.position
system1.property

If you still want to be able to access both system1.forces and system1.property you could add another condition to the getproperty() function or just settle with using getfield(system1, :property).

Edit:

We seem to have come to the same conclusion :sweat_smile:

1 Like

You can have a constructor that accepts keyword arguments, and have the user call it as System(; forces).

julia> function System(; kwargs...)
         p = only(kwargs)
         p.first in (:forces, :energy) || throw(ArgumentError("Unknown keyword argument $(p.first)"))
         @eval function Base.getproperty(s::System, a::Symbol)
           if a == $p.first
             getfield(s, :property)
           else
             getfield(s, a)
           end
         end
         System(p.second)
       end
System

julia> system = System(; forces)
System{Vector{Float64}}([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])

julia> system.forces
10-element Vector{Float64}:
...

System(; forces) is effectively equivalent to System(; forces = forces), so it’s a nice shortcut to get both the variable name and the value.

A disadvantage is that now getproperty lives inside this constructor which is a bit weird - but if you’re not using System’s getproperty for anything else, it’s not too bad.

1 Like

Interesting idea, although I think we should avoid using @eval. For now the optional name field seems a safer solution (and I do need to access the fields by the original name).

Define a type Energy and a type Forces like

forces = Forces(zeros(10))
system = System(forces)

And use dispatch

3 Likes

Energy and Forces are examples, the data can be anything, actually. It would be on the user’s side to implement that custom type and dispatch.

It seems that, if you combine the techniques from above with your own you arrive at a solution that pretty much does what you want, while avoiding the redundancy of separately specifying the property name (provided you don’t mind storing the name in an extra field). I’m afraid, that it will always be either storing the name explicitly, using eval or using a macro.

struct System{T}
    property_name::Symbol
    property::T
end

function System(; prop...)
    name, value = only(prop)
    return System(name, value)
end

function Base.getproperty(s::System, a::Symbol)
    a == getfield(s, :property_name) && return getfield(s, :property)
    return getfield(s, a)
end

energy = 1e6
velocity = [1e1, 2e2, 3e3]

sys1 = System(; energy)
@assert sys1.energy == sys1.property

sys2 = System(; velocity)
@assert sys2.velocity == sys2.property

I hope this helps!
3 Likes

That is interesting, and may be useful to create what I want (I cannot use it directly because the System struct has other fields).

But what is the magic with this name, value = only(prop)? I cannot make that command work outside the specific context of the struct. Is that documented?

I am not 100% sure I understand your question, there is no magic involved. only gives you the only element of a collection, if there are more than one, it throws an exception. And since the one element is a Pair (:key => value) you can destructure it like k,v = :a=>1 .

But if you change your struct according to

struct System{T}
    property_name::Symbol
    property::T
    additional_field::Int
end

function System(add_field; prop...)
    name, value = only(prop)
    return System(name, value, add_field)
end

it should work the same.

1 Like

Oh, I see, the magic is this one:

julia> f(;p...) = @show p
f (generic function with 1 method)

julia> x = rand(2);

julia> f(;a = x, b = 2.0)
p = Base.Pairs{Symbol, Any, Tuple{Symbol, Symbol}, NamedTuple{(:a, :b), Tuple{Vector{Float64}, Float64}}}(:a => [0.2648247366377978, 0.8191610433834756], :b => 2.0)
pairs(::NamedTuple) with 2 entries:
  :a => [0.264825, 0.819161]
  :b => 2.0

julia> f(;x)
p = Base.Pairs(:x => [0.2648247366377978, 0.8191610433834756])
pairs(::NamedTuple) with 1 entry:
  :x => [0.264825, 0.819161]

When the keyword parameters are not pairs, the symbol of the variable is known by the function, but it isn’t when actual pairs are provided.

Maybe I can use that, yes, not sure if it will be clearer to the user than just explicitly give the name of the property as an independent parameter. Nice thing to learn, anyway, thanks.

Yes, concerning your last example it is equivalent to write f(; x) and f(; x=x).

So the following would also work:

en=1e6

sys = System(; energy=en)

@assert sys.property == sys.energy