Extracting kwargs from anonymous function

EDIT: original title was “Confused about escaping in macros”

I’m trying metaprogramming in Julia for the first time, and I’m having some trouble grasping the escaping and hygiene discussion in the manual.

I’m trying to write a macro @par(func, params...) that inserts some params... as kwargs in an anonymous function func. So, I want to make

@par((x,y) - > x * y * a, a)

become

(x, y; a) -> x * y * a

After some fumbling I have what seems like a working macro:

macro par(f, params...)
    f.head == :-> || throw(ArgumentError("Only @par(args -> ..., parameters...) syntax supported"))
    farg = f.args[1]
    body = f.args[2]
    symbolarg = isa(farg, Symbol)
    symbolarg || isa(first(farg.args), Symbol) || throw(ArgumentError("Keyword arguments not supported"))
    N = symbolarg ? 1 : length(farg.args)
    newargs = symbolarg ? (esc(farg),) : esc.(Tuple(farg.args))
    newparams = esc.(params)
    newbody = esc(body)
    newf = :(($(newargs...), ; $(newparams...)) -> $(newbody))
    return newf
end

I am however quite unsure that I’m doing things as I should regarding the use of esc and hygiene. Could somebody have a look at the above to see if there is any glaring problem?

1 Like

Yes, this looks like you’ve done things correctly as far as I can tell. In terms of Hygiene, it seems that you’ve escaped everything in the output, so you could have just not done all that intermediate escaping and just had the last line been return esc(newf).

Another general comment is that the package ExprTools.jl may be quite helpful for your purposes here.

Thanks @Mason!

I am a bit perplexed: it seems to me that (x, y; a) -> x * y * a is quite readable, better than the application of the macro. What is the motivation?

1 Like

Very good question! I want to be able to extract the kwargs from an anonymous function. I believe the only way is to use a macro at parse time. As soon as (x, y; a) -> x * y * a is compiled, there is no way to know that the list of kwargs is (:a,). If there were a way, I would be really happy to learn how!

EDIT: In other words, I want to use @par as an alternative syntax to write anonymous functions because of its side effect of providing their kwargs as the params... arguments

Maybe in that case, the macro should just take in (x, y; a) -> x * y * a and then return

(x, y, ; a) -> (params = (;a=a), x * y * a)

i.e. it would take in standard syntax.

1 Like

Mmm, but that would not give you access to params without evaluating the function, right?

My current full implementation is this

struct ParametricFunction{N,M,F}
    f::F
    params::NTuple{M,Symbol}
end

ParametricFunction{N}(f::F, p::NTuple{M,Symbol}) where {N,M,F} = ParametricFunction{N,M,F}(f, p)

(pf::ParametricFunction)(args...; kw...) = pf.f(args...; kw...)

parameters(p::ParametricFunction) = p.params

macro par(f, params...)
    (f isa Expr && f.head == :->) ||
        throw(ArgumentError("Only @par(args -> ..., parameters...) syntax supported"))
    farg = f.args[1]
    body = f.args[2]
    symbolarg = isa(farg, Symbol)
    symbolarg || isa(first(farg.args), Symbol) ||
        throw(ArgumentError("Keyword arguments not supported in @par"))
    N = symbolarg ? 1 : length(farg.args)
    newargs = symbolarg ? (esc(farg),) : esc.(Tuple(farg.args))
    newparams = esc.(params)
    newbody = esc(body)
    newf = :(($(newargs...), ; $(newparams...)) -> $(newbody))
    return :(ParametricFunction{$N}($newf, $params))
end

So now

julia> f = @par((x, y) -> x*y*a, a)
ParametricFunction{2,1,var"#4#6"}(var"#4#6"(), (:a,))

julia> parameters(f)
(:a,)

julia> f(2, 3; a = 3)
18

Here’s how I would do this:

using ExprTools: splitdef
struct ParametricFunction{N,M,F}
    f::F
    params::NTuple{M,Symbol}
end

ParametricFunction{N}(f::F, p::NTuple{M,Symbol}) where {N,M,F} = ParametricFunction{N,M,F}(f, p)

(pf::ParametricFunction)(args...; kw...) = pf.f(args...; kw...)

parameters(p::ParametricFunction) = p.params


macro par(f::Expr)
    (f.head == :->) || throw(ArgumentError("Only @par(args -> ..., parameters...) syntax supported"))
    d = splitdef(f)
    kwargs = get!(d, :kwargs, []) #use get! because the :kwargs might not exist
    params = (x -> x isa Symbol ? x : x.args[1]).(kwargs) |> Tuple
    N = length(get!(d, :args, []))
    esc(:(ParametricFunction{$N}($f, $params)))
end

and then


julia> @macroexpand @par (x, y; a) -> x * y * a
 :(ParametricFunction{2}(((x, y; a)->begin
               #= In[90]:38 =#
               x * y * a
           end), (:a,)))

julia> pf = @par (x, y; a) -> x * y * a
ParametricFunction{2,1,var"#74#76"}(var"#74#76"(), (:a,))

julia> parameters(pf)
(a,)

julia> pf(1, 2, a=3)
6

To be clear, there’s nothing wrong with the syntax you’re using, I just personally find it easier to understand the kwarg syntax.

2 Likes

I love that, much better!!

1 Like

@Mason, do you mind if I copy your code into my package?
(MIT license: GitHub - pablosanjose/Quantica.jl: Simulation of quantum systems on a lattice)

Not at all. :slight_smile:

1 Like

You don’t even need the ParametricFunction type if you don’t want — you could just have your macro effectively do:

f = (x, y; a) -> x * y * a
parameters(::typeof(f)) = (:a, )
return f
3 Likes

Clever!