Macros to fill keyword arguments

Hi everyone,

I’m having a difficult time understanding how to properly use macros. I’m trying to make a macro @passkwargs that will turn

foo(; @passkwargs(x,y)) into foo(; x=x, y=y)

I’m just starting to work with macros and I figured this would be a useful and relatively easy. Unfortunately I’m having a really difficult time navigating variable scope.

I’ve focused around using the idea that you can pass a Pair(Symbol, Any) as a keyword argument. My first try was this:

macro passkwargs(args...)
    result = (Pair(i,i) for i in args)
    return :($(result)...)
end

But instead of getting the desired result, it gives the symbols as both keywords and values. So I tried to evaluate the second item in the pair

macro passkwargs(args...)
    result = (Pair(i,eval(i)) for i in args)
    return :($(result)...)
end

but of course it evaluates the variable in the global scope, and not the scope of the function for which it is called. I’ve since been trying to figure out how esc() works but for some reason its not clicking - I get the Symbol as the keyword but the Expr is the value.

How do I do this? Any help would be appreciated

Here’s one solution – the trick is that I think you need QuoteNode to make the symbols it receives stay symbols in the output:

macro p3(args...)
    sym = QuoteNode.(args)
    :( $NamedTuple{($(sym...),)}(($(args...),)) ) |> esc
end

using NamedTupleTools # for this constructor

A = rand(1:9, 3,4)
dims = 1

@macroexpand @p3(dims)

sum(A; @p3(dims)...) # needs the ; and ...

If you paste this into the REPL it should work find without the esc, but if it were in a module, then it would look for dims there, not in your scope, if you left this out. The reason for writing $NamedTuple is to put this back in the module’s scope, so that the caller need not have loaded it.

2 Likes

Thanks to @mcabbott because this solution does work well and put me on the right path. However, I’ve been snooping and found a more optimal solution just in case anyone runs into this post in the future. I discovered the :kw symbol, which, when used as the head of an expression designates a keyword pair, which is perfect. The new macro is this:

macro passkwargs(args...)
    kwargs = [Expr(:kw, arg, arg) for arg in args]
    return esc( :( $(kwargs...) ) )
end

The benefit of this solution is it can be used anywhere in the list of arguments and does not need a ; before it, although doing so is perfectly fine.

edit - apparently this only works for a single kwarg, and I’m trying to figure out why

3 Likes

Could anyone repeat this solution?
It would be so nice if this worked out, but I receive an error as soon as I pass more than one argument.

julia> macro passkwargs(args...)
           kwargs = [Expr(:kw, arg, arg) for arg in args]
           return esc( :( $(kwargs...) ) )
       end

julia> f(args...; kwargs...) = println(args, "\n", Dict(kwargs))
f (generic function with 1 method)

julia> x = 1; y = 2

julia> f(@passkwargs(x))
()
Dict(:x => 1)

julia> f(@passkwargs(y))
()
Dict(:y => 2)

julia> f(@passkwargs(x, y))
ERROR: MethodError: no method matching esc(::Expr, ::Expr)

You don’t need a macro for this anymore. Just call the function as f(; x, y).

julia> f(args...; kwargs...) = println(args, "\n", Dict(kwargs));

julia> x = 1; y = 2;

julia> f(; x)
()
Dict(:x => 1)

julia> f(; y)
()
Dict(:y => 2)

julia> f(; x, y)
()
Dict(:y => 2, :x => 1)
1 Like

Thanks for your reply. My use case is a bit different, but very similar.
I want to supply multiple kwargs by calling a single macro.
I know that it is possible if I splat outside the macro call, but that is not convenient for my use case, because the second argument of the macro is long. E.g. I’d like to

f(x, a = "a", @g y [
    card(1)
    card(2)
])

to result in

f(x, a = "a", y = y, f2([
    card(1)
    card(2)
]))

So the macro splatting should work without preceding ‘;’ and should supply first a kwarg and then a positional argument.
Unfortunately, passing :((Expr(:kw, :y, y), f2(x2))...) nor :((:y => y, f2(x2))...) works because splatting of Pairs into kwargs only works after a ‘;’ character.
I think something like `esc(x, y) would need to be allowed from the Julia side.