mutable struct Test
a::Float64
b::Float64
c::Float64
end
Test(a,b) = Test(a,b,a+b)
I had expected when I update either of fields a or b that c would also be re-calculated. This doesn’t appear to be the case. I.e.
eg = Test(1,2)
eg.a = 2
In this case I would expect eg.c = 4 but it is still = 3.
Is there a way to force the recalculation? Or a better way to do this in general?
One solution would be to re-create a non-mutable struct every time instead of updating the values. Is this a reasonable solution?
One way to achieve that is creating “virtual” properties, which are not actually stored inside the struct by overloading the getproproperty function.
mutable struct MyStruct
a::Float64
b::Float64
end
function Base.getproperty(s::MyStruct, f::Symbol)
if f===:c
return s.a + s.b
else
return getfield(s, f)
end
end
s = MyStruct(1,2)
s.a # 1
s.b # 2
s.c # 3
s.a = 4
s.c # 6
This has the drawback, that you’re not memoizing the results but recalculate c whenever you need it. Another similar option would be to overload Base.setproperty! in a way, that it updates c whenever someone changes field a or b.
mutable struct MyStruct2
a::Float64
b::Float64
c::Float64
end
MyStruct2(a,b) = MyStruct2(a,b,a+b)
function Base.setproperty!(s::MyStruct2, name::Symbol, x)
setfield!(s, name, x)
setfield!(s, :c, s.a + s.b)
end
s = MyStruct2(1,2)
s.a = 4.0
s.c # 6
The calculation of a+b is just something that happens when the constructor, Test, is called. And the constructor is not called when you update a field, since there is no construction of a new object going on. The definition of the constructor does not create some sort of ‘link’ between the fields a, b and c.
The function that is called when you modify a field is setproperty!, and you will have to work with that function to achieve something like what you want, as shown by @hexaeder.
The solution above is very interesting, but I think it is much more common just to write a function as:
julia> function update!(t::Test;a=t.a,b=t.b)
t.a = a
t.b = b
t.c = a + b
return t
end
update! (generic function with 1 method)
julia> update!(t;a=2)
Test(2.0, 2.0, 4.0)
If you are open to external dependencies with Observables.jl you can do exactly this just with some extra syntax:
using Observables
struct Test
a::Observable{Float64}
b::Observable{Float64}
c::Observable{Float64}
function Test(a::T,b::T) where T<:Real
ao = Observable{Float64}(a)
bo = Observable{Float64}(b)
co = Observables.@map &ao + &bo;
new(ao,bo,co)
end
end
Now do
julia> eg = Test(1,2)
Test(Observable(1.0), Observable(2.0), Observable(3.0))
julia> eg.a[] = 2
2
julia> eg.c[] #and you get what you want
4.0
Just be careful because then you are not mutating the object, just creating a new one. Actually this might be the most performant way, if you make the object to be immutable.