Concise way to initialize field in terms of another field

Suppose I have:

Base.@kwdef struct Example
    a::Int
    b::Int
end

I would like the initialization of b to refer to the initial value of a, but the following does not work:

x = Example(a = 6, b = a+9)

because the “a” that appears in the definition of b does not refer to the field named a but to some external (undefined) a. Is there a clean way for the initialization of one field to refer to the value stored in a previously defined field? In C++, the keyword “this” addresses this situation.

You could define a single-argument constructor method for this:

julia> Example(a) = Example(a, 9+a)
Example

julia> Example(3)
Example(3, 12)

it just works, if you write the constructor function manually:

julia> function f(a,b=1+a)
           a, b
       end
f (generic function with 2 methods)

julia> f(3)
(3, 4)

Thanks for the prompt answers to my question! To clarify, the actual struct in my code has many fields (more than 20), and the relationships among them do not follow any particular pattern. Hence it would be awkward to solve the problem with custom constructors, especially since the code is under development and the precise relationships between the fields may change over time.

at syntax level this will never work, because a+9 needs to be computed first.

The best you can do is make a macro yourself that would transform this to:

let a=6, b=a+9
    x = Example(; a, b)
end

since it looks like let would resolve it in order:

julia> let a = 9, b=a+9
           a,b
       end
(9, 18)
1 Like

OK, I tried to code your suggestion, but it didn’t work. As you can see from the run below, the code reproduces the same problem I had originally, namely, that “a” that appears in the assignment statement b=a+2 does not refer to the “a” that was just initialized. I’m not an expert on macros, and I’m not sure why the macro-expander decided that “a” in one place is a new temporary variable and in the other place is a previously defined variable. Maybe I need to throw in some escapes?

macro kwconstruct_helper(expr::Expr)
    @assert expr.head == :call
    fname = expr.args[1]
    kwname = Any[]
    kwrhs = Any[]
    numassgn = length(expr.args) - 1
    for j = 2 : numassgn + 1
        @assert expr.args[j].head == :kw
        push!(kwname, expr.args[j].args[1])
        push!(kwrhs, expr.args[j].args[2])
    end
    letassignments = Expr[]
    kwargs = Expr(:parameters)
    for i = 1 : numassgn
        push!(letassignments, Expr(:(=), kwname[i], kwrhs[i]))
        push!(kwargs.args, kwname[i])
    end
    return Expr(:let,
                Expr(:block, letassignments...),
                Expr(:block, __source__,
                     Expr(:call, fname, kwargs)))
end

Output from

@macroexpand @kwconstruct_helper Example(a=7, b=a+2)

is

let var"#36#a" = 7, var"#35#b" = Main.test_kwconstruct.a + 2
    #= u:\JuliaFEM\CohesiveFEM\src\kwconstruct_helper.jl:40 =#
    Main.test_kwconstruct.Example(; a = var"#36#a", b = var"#35#b")
end

In case someone returns to this post: The problem with the macro I posted previously is that it is missing some esc calls. Here it is again corrected.

macro kwconstruct_helper(expr::Expr)
    @assert expr.head == :call
    fname = expr.args[1]
    kwname = Any[]
    kwrhs = Any[]
    numassgn = length(expr.args) - 1
    for j = 2 : numassgn + 1
        @assert expr.args[j].head == :kw
        push!(kwname, expr.args[j].args[1])
        push!(kwrhs, expr.args[j].args[2])
    end
    letassignments = Expr[]
    kwargs = Expr(:parameters)
    for i = 1 : numassgn
        push!(letassignments, Expr(:(=), esc(kwname[i]), esc(kwrhs[i])))
        push!(kwargs.args, esc(kwname[i]))
    end
    return Expr(:let,
                Expr(:block, letassignments...),
                Expr(:block, __source__,
                     Expr(:call, fname, kwargs)))
end

Here is a small test. The statements:


Base.@kwdef struct Example
    a::Int
    b::Int
end
println(@kwconstruct_helper Example(a=7, b=a+2))

produce the output

Main.test_kwconstruct.Example(7, 9)
1 Like