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.
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
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.
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.
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.
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.
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.
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.
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.