Best method for having a single number that is mutable

I often find myself doing the following:

struct MyType
    x::Float64
    some_other_fields
end

where I want to sometimes change the value of x. What’s the best way to deal with this? It seems a bit silly to rebuild a MyType every time I change x, as there might be lots of other fields. So instead, I currently do:

struct MyType
    x::Vector{Float64}
    some_other_fields
end

where x is a single element vector, which allows me to mutate as much as I want.

Is this wasteful? Is there a more efficient way to handle this situation?

Cheers,

Colin

just use a mutable struct (with const for the fields that don’t change)

3 Likes

Thanks for responding. Just to be clear, do you mean:

mutable struct MyType
    x::Float64
    const some_other_fields
end

The other possibility I guess is:

mutable struct MutableFloat
    x::Float64
end
struct MyType
    x::MutableFloat
    some_other_fields
end

Any thoughts on whether that is better? Or is it equivalent for all practical purposes?

If x just needs to hold a single element, you can use the following.

struct MyType
    x::Base.RefValue{Float64}
    some_other_fields
end

You can use it as follows.

julia> my_type = MyType(Ref(5.0), 1)
MyType(Base.RefValue{Float64}(5.0), 1)

julia> my_type.x[] = 3.2
3.2

julia> my_type
MyType(Base.RefValue{Float64}(3.2), 1)

julia> my_type.some_other_fields = 2
ERROR: setfield!: immutable struct of type MyType cannot be changed

If you really wanted to retain the syntax, you can use Oscar’s suggestion or do the following.

julia> function Base.getproperty(obj::MyType, s::Symbol)
           if s == :x
               getfield(obj, s)[]
           else
               getfield(obj, s)
           end
       end

julia> function Base.setproperty!(obj::MyType, s::Symbol, x)
           if s == :x
               getfield(obj, :x)[] = x
           else
               setfield!(value, s, x)
           end
       end

julia> my_type.x = 3.1
3.1

julia> my_type.x
3.1

julia> my_type
MyType(Base.RefValue{Float64}(3.1), 1)

Extra note:

Base.RefValue{T} is similar to a 0-dimensional array, Array{T, 0}. Both hold a single element.

julia> zero_dim_array = Array{Float64, 0}(undef)
0-dimensional Array{Float64, 0}:
0.0

julia> zero_dim_array[] = 5
5

julia> zero_dim_array
0-dimensional Array{Float64, 0}:
5.0
4 Likes

Oh that’s neat. I never thought of using RefValue. I think that’s my new favourite solution. Thanks for the response.

Accessors.jl lets you do the rebuilding with the simpler syntax of fieldwise mutation. With simpler constructors like the defaults, you won’t need to implement underlying methods to make it work, and the compiler is pretty good at picking structs apart and optimizing constructors so that instantiation doesn’t actually rebuild from scratch. I don’t have any sources comparing performance, but if you don’t need an instance and its changes to be shared by multiple bindings, Accessors lets you avoid the heap allocation overheads of instantiating mutable instances, whether of mutable struct MyType or x::Base.RefValue{Float64}. Such overhead is only significant if you’re instantiating and discarding many mutable instances; nearly zero such overhead occurs at the other extreme where you instantiate 1 MyType instance and mutate it for the rest of the program. I should stress that Accessors still instantiates at each rebuild rather than rote copying, so if the constructor takes a lot of time (you can test this with sleep), then it can be preferable to use mutation to avoid the instantiation overhead.

3 Likes

Oh that’s cool too. I’ll check it out in more detail, thanks for the link.