Parametric Field Names

Consider Following Situation:

I want to model an object with struct in Julia. I want this object fields be mutable. The goal is that when we change one of the fields, the rest of the fields will change automatically.

To define a specific problem, consider a dummy structure Square, The field names are: side_length and area. I want when I Change side_length the area changes automaticly. I know this is not great example for real world use cases

The following Code is definition of this structure:

mutable struct Square
    side
    area
end

s1 = Square(2, 4)

s1.side = 5

println(s1.area) # Obviously the output is 4

I am also curious to know if it is possible for some fields to be made automatically at the time of instantiation? for example at the creation time we only specify the side_length of this structure.

Your first question is not easily answered, although I think there are ways to achieve this.

The second question is simple, just define a constructor:

julia> mutable struct Square
           side
           area
       end

julia> Square(side) = Square(side, side^2)
Square

julia> Square(5)
Square(5, 25)
2 Likes

https://docs.julialang.org/en/v1/manual/constructors/#man-inner-constructor-methods

The inner constructor is what you use for this kind of things:

It is good practice to provide as few inner constructor methods as possible: only those taking all arguments explicitly and enforcing essential error checking and transformation.

your example falls under “enforcing essential transformation”


julia> mutable struct Square
           side
           area
       Square(s) = new(s, s^2)
       end

julia> Square(3)
Square(3, 9)

In this case, we can even deliberately disallow the 2-argument constructor because the info is redundent. (assuming it’s useful for Square to carry an .area field

3 Likes

I don’t think you will be able to override Base.setproperty! with this kind of behavior. I just tried it and ended up in a nest of StackOverflow errors.

Best to do a changesidelength!(x) method which does both steps.

1 Like

You could perhaps accomplish your first goal by making use of either set methods or making the struct fields Observable using Observables.jl and attach the recalculation behavior to the observable callback.

1 Like

What if you implement setproperty using setfield rather than recursively calling setproperty?

2 Likes

Seems like setproperty! is the way to go (using setfield! internally to avoid the stack-overflow).

E.g.

mutable struct Square
   side
   area
end
function Base.setproperty!(x::Square, field::Symbol, val)
  if field == :side
    setfield!(x, :side, val)
    setfield!(x, :area, val^2)
  else
    # .. or whatever else you want to do here
    error("oops!")
  end
end

s1 = Square(2,4)
s1.side = 5
println(s1.area)
# output is 25

Alternatively, you could define a set of “derrived” properties that have no fields in the struct, and override propertynames and getproperty and use those to generate the derived properties.

5 Likes

Why should it be inner constructor?

julia> mutable struct Square
           side
           area
       end

julia> Square(side) = Square(side, side^2)
julia> Square(3)
Square(3, 9)

in your code here, try Square(3, 10). Now you have an illegal instance

3 Likes

Ah, you mean if someone want to lock users out from instantiating some fields directly? Yes, it makes sense than.

One should override area field also, so it can be accessed read only.

How would you implement a setside!(s::Sqaure, len) if you do this? also you can’t make something truly read-only anyways, just don’t bother. If someone insists, they can break Julia itself easily.

Yes, you are right again. But in this case inner constructor is not providing real protection, so it’s ok to use outer constructor.

s = Square(3)
s.area = 10

Just two lines instead of one. Hardly worth inner constructor (especially taking into account that in order to change it one needs to restart REPL).

User should know not to do this. In some class-based OOP, this is “protected” by making s.area a “private” variable and s.get_area() to be “read-only”. But in the end of the day it only increase development overhead. You have to trust user not to deliberately digging holes for themselves at some point anyway.

1 Like

DataFrames has checks for corrupted DataFrames. Before operations, it makes sure all vectors are the right size. OP could also implement this.

But really, @dariush-bahrami , just store the side-length and use helper functions to do all that stuff for you.

Personally I’am not a huge friend of storing redundant data, and you can avoid any illegal instance by just storing the essential information once. Found this originally here and meanwhile using this quite extensively:

mutable struct Square
    side
end

# Acutally not strictly needed just for completeness
# see https://docs.julialang.org/en/v1/base/base/#Base.propertynames   
propertynames(p::Square,private=true) = begin
    fields = fieldnames(typeof(p))    
    (fields...,:area,:diagonal)
end

#Dispatch the gets
Base.getproperty(p::Square,n::Symbol) = getproperty(p::Square,Val{n}())
Base.getproperty(p::Square,::Val{S}) where {S} = getfield(p,S)  #generic fallback
Base.getproperty(p::Square,::Val{:area}) = p.side^2
Base.getproperty(p::Square,::Val{:diagonal}) = √2*p.side

#Dispatch the sets
Base.setproperty!(p::Square,n::Symbol,x) = setproperty!(p,Val{n}(),x)
Base.setproperty!(p::Square,::Val{S},x) where {S} = setfield!(p,S,x)     #generic fallback
Base.setproperty!(p::Square, ::Val{:area},x) = setfield!(p,:side,√x)
Base.setproperty!(p::Square, ::Val{:diagonal},x) = setfield!(p,:side,x/√2)



julia> s = Square(√2)
Square(1.4142135623730951)

julia> s = Square(√2)^C

julia> s.diagonal
2.0000000000000004

julia> s.area=4
4

julia> s.diagonal
2.8284271247461903

julia> 

It is also easier to maintain this way and you can easily add up more implicit properties without stressing your memory too much.

2 Likes

I agree that can make a lot of sense. That was the reason for the “Alternatively, you could define a set of “derrived” properties that have no fields in the struct, and override propertynames and getproperty and use those to generate the derived properties.” part of my comment.

I think which route you go—the setproperty! or the getproperty route—depends a lot on the exact tradeoffs of the problem. That is, do you care more about memory or compute complexity for the actual problem you want to solve.

1 Like

Thank you very much. This solution seems to be the most straightforward.