Mutable struct and function as parameters. Why can't the function access the new values?

Mutable struct and function as parameters. Why can’t the function access the new values?

Both calls to p.f() return the same value, 2.0, despite the new value of x

@with_kw mutable struct Params
     x = 1.0
     f::Function = () -> 2*x 
 end
p=Params();
p.f()
p.x=10
p.x
p.f()

Looking at the @macroexpand, something like this happens:

julia> mutable struct Xf
         x::Float64
         f::Function
       end

julia> foo(; x = 1.0, f = (() -> 2*x)) = Xf(x, f)
foo (generic function with 1 method)

julia> xf = foo()
Xf(1.0, var"#22#24"{Float64}(1.0))

julia> xf.x, xf.f()
(1.0, 2.0)

julia> xf.x = 10; xf.x, xf.f()
(10.0, 2.0)

The anonymous function captured the x in the foo function; in other words they share that variable. But the Xf constructor does not share the variable x, the call Xf(x, f) just has the variables pass their instances to Xf’s own local variables. The Xf.x field is thus independent of the captured variable x. It’s an understandable confusion given how closures work in function scopes, but despite how the macro makes it look, field definitions aren’t variables that could be captured by inner constructors.

Incidentally, the order of the keyword arguments and thus your fields is important for successful capturing:

julia>  foo(;f = (() -> 2*x), x = 1.0) = Xf(x, f)
foo (generic function with 1 method)

julia> xf = foo() # xf.f didn't bother capturing anything
Xf(1.0, var"#12#14"())

julia> xf.f()
ERROR: UndefVarError: x not defined

To accomplish what you seem to be going for, you could do this:

mutable struct Params
  x::Float64
end

(p::Params)() = 2*p.x

p = Params(1.0)
p() # 2.0
p.x = 10
p() # 20.0

# if you rather not have p itself be callable
f(p::Params) = 2*p.x # do f(p) instead of p.f()
1 Like

The x that is captured is the keyword from the constructor. You could set it up this way though:

mutable struct Params
    x::Float64
    f::Function
    function Params(x = 1.0)
        p = new(x)
        p.f = ()->p.x*2
        return p
   end
end

julia> mutable struct Params
           x::Float64
           f::Function
           function Params(x = 1.0)
               p = new(x)
               p.f = ()->p.x*2
               return p
          end
       end

julia> p = Params(1.5)
Params(1.5, var"#7#8"{Params}(Params(#= circular reference @-2 =#)))

julia> p.f()
3.0

julia> p.x = 2
2

julia> p.f()
4.0

Making Params a functor like Benny suggested is probably better though.

2 Likes