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?
affans
July 10, 2022, 12:10am
2
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
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!