Creating a method dynamically with keyword arguments

I have a context structure with many fields and want to mutate some by reinitialising, where I could do various validations. I know there is an excellent SetField.jl package, but I have only one structure, and thus, I would like to avoid introducing additional dependencies. Thus I am attempting to construct the following code with an eval:

struct Context
    a::Int
    b::Int
end

function Context(ctx; a=ctx.a, b=ctx.b)
    return Context(a, b)
end

ctx = Context(2, 4)
new_ctx = Context(ctx, a=3)

So far the best I have come up with is using the strings and at the last stage calling Meta.parse:

function_signature = join(["$(f)=ctx.$(f)" for f in fieldnames(Context)], ", ")
function_body = join(["$(f)" for f in fieldnames(Context)], ", ")

body = """
function Context(ctx::Context; $function_signature)
    return Context($function_body)
end
"""

eval(Meta.parse(body))

But this did not work out, although the body contains the exact definition I need. I would also much prefer using expressions directly. Can someone help me with this?

Is it time critical? Does it have to involve metaprogramming?

If not Iā€™d try something like

julia> @kwdef struct Context
           a::Int
           b::Int
       end
Context

julia> function Context(ctx::Context; kwargs...)
           return Context(; merge(Dict(f => getfield(ctx, f) for f in fieldnames(Context)), kwargs)...)
       end
Context

julia> ctx = Context(2, 4)
Context(2, 4)

julia> new_ctx = Context(ctx, a=3)
Context(3, 4)
1 Like

Looks good. Although I would like to see how the metaprpgraming approach would look like.

I made a variation of your approach which does not require to use @kwdef:

struct Context
    a
    b
end

function Context(ctx::Context; kwargs...)
    return Context([get(kwargs, key, getfield(ctx, key)) for key in fieldnames(Context)]...)
end

ctx = Context(2, 3)
Context(ctx; a=4)

The problem is however that Context(ctx; c=4) does not error. So I also need to add also validation check for kwargs.

Solved it

@eval begin
    function Context(ctx::Context; $([Expr(:kw ,k, :(ctx.$k)) for k in fieldnames(Context)]...))
        return Context($(fieldnames(Context)...))
    end
end