Changing field with a function in a mutable struct

Hello all!

I have a mutable struct that must carry a function in one of its fields. Once I assign it to a variable, however, I cannot change the function. Here’s an example:

mutable struct Foo{T<:Real,F<:Function}
    a::T
    f::F
end
bar = () -> 1
baz = () -> 2
foo = Foo(1,bar)
foo.f = baz

Which results in the following error:

ERROR: MethodError: Cannot `convert` an object of type var"#121#122" to an object of type var"#119#120"

I solved this by creating a new variable with the desired function and then assigning all fields except the function from the old variable to the new:

foobar = Foo(2,baz)
for name in fieldnames(Foo)
    if !(name === :f)
        setfield!(foobar,name,getfield(foo,name))
    end
end

However, this seems overly convoluted and naive. I was wondering if there is a simpler way?

In Julia, each function is its own type, and so when you instantiate the struct, f becomes a fixed type and when you try to assign a different function to it, you are actually changing the type of that field. It’s no different than doing

mutable struct Foo 
   a::Int64 
end 
foo = Foo(1)
foo.a = "hello world" # fails 

However, I do not know how to answer your particular problem, though I am pretty sure it has a solution.

3 Likes

I appreciate the input!

Not sure why you need the function type in the type signature of your struct. But if that isn’t essential then this alternative will work:

mutable struct Foo{T<:Real}
    a::T
    f::Function
end

Foo is a concrete type so I would expect it to be efficient:

julia> foo = Foo(1,bar)
Foo{Int64}(1, var"#9#10"())

julia> foo.f = baz
#11 (generic function with 1 method)

julia> isconcretetype(typeof(foo))
true

@code_warntype suggests there might be a type instability but @code_llvm and @code_native both look very efficient.

This function benchmarks well

f(a,f1,f2) = (a.f === f1) ? a.f = f2 : a.f = f1

julia> @benchmark f($foo,$bar,$baz)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  1.800 ns … 23.100 ns  β”Š GC (min … max): 0.00% … 0.00%
 Time  (median):     2.100 ns              β”Š GC (median):    0.00%
 Time  (mean Β± Οƒ):   2.161 ns Β±  0.499 ns  β”Š GC (mean Β± Οƒ):  0.00% Β± 0.00%

  β–…  β–„      β–ˆ   β–„   β–„  β–…          ▁          ▁               ▁
  β–ˆβ–β–β–ˆβ–β–β–β–ˆβ–β–β–ˆβ–β–β–β–ˆβ–β–β–β–ˆβ–β–β–ˆβ–β–β–β–‡β–β–β–ˆβ–β–β–β–ˆβ–β–β–β–…β–β–β–†β–β–β–β–ˆβ–β–β–β–β–β–β–ƒβ–β–β–β–…β–β–β–† β–ˆ
  1.8 ns       Histogram: log(frequency) by time      3.4 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

so it looks like the compiler is generating efficient code for accessing the function field.

5 Likes

Thank you for the solution! Indeed I don’t recquire the function type in the type signature, I just read elsewhere that declaring a struct as such could improve efficiency.

Much appreciated!

The moment you decided to store a Function inside the struct you kinda already thrown efficiency out of the window, so I would not worry.

Hmm, perhaps I could store the function symbol and have it evaluated with @eval and such later on?

Do you really need the struct to be mutable? It’s easy to make a new Foo instance with the f field replaced:

julia> using Accessors

julia> struct Foo{T<:Real,F<:Function}
           a::T
           f::F
       end

julia> bar = () -> 1
#1 (generic function with 1 method)

julia> baz = () -> 2
#3 (generic function with 1 method)

julia> foo = Foo(1,bar)
Foo{Int64, var"#1#2"}(1, var"#1#2"())

julia> newfoo = @set foo.f = baz
Foo{Int64, var"#3#4"}(1, var"#3#4"())

Here, everything stays type-stable and performant.

2 Likes

This probably would be worse, eval is even slower.

Oooh that’s a nice take! I wasn’t aware of the Accessors package. Much appreciated!