Unexpected result with mutable structs

Hi there,

I have a mutable struct defined as follows:

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?

Thanks for any help.

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

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.

4 Likes

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)
4 Likes

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

notice the extra [] bracket syntax.

That’s some heavy (but very cool) machinery for this.

Thanks. This seems like the neatest approach (although maybe just re-calling the constructor is easier still).

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.