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()