Unpack NamedTuple into a function definition

I am writing a Julia package to implement a REST API, and I have the accepted parameters available as NamedTuple constants. I’d like to have these NamedTuple constants double as the default keyword arguments for a function. I’d rather not just write a generic kwargs... in the definition, because it would be handy to have the user be able to tab-complete the acceptable parameters.

Is this possible, possibly with macro magic? I’d like the two definitions of query below to be identical at runtime.

const PARAMETERS = (; a = 1, b = 2)

function query(; PARAMETERS...)
  ...
end

function query(; a = 1, b = 2)
  ...
end

I’ve tried the following, and I think I’m close, but for some reason the character " is being interpolated into the expression.

julia> @eval f(; $(replace(string(p), "("=>"", ")"=>""))) = a + b
ERROR: syntax: invalid keyword argument syntax ""a = 1, b = 2""
Stacktrace:
 [1] top-level scope
   @ none:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:370
 [3] top-level scope
   @ REPL[46]:1
1 Like

This is an interesting question! I’ll point out some suspected confusion: when metaprogramming in Julia, code is not represented with strings, but rather expression trees. If you interpolate a string into @eval, you’ll get a literal string:

julia> e = "2 + 2"
"2 + 2"

julia> :( a = $e ) # this is what `@eval a = $e` would execute
:(a = "2 + 2")

julia> e = :(2 + 2) # instead, deal with expressions
:(2 + 2)

julia> :( a = $e )
:(a = 2 + 2)

You’d never represent code as a string unless it comes to you that way — in which case you can Meta.parse it into an expression.


On to the question: you have a named tuple which you want to use as default keyword arguments for some method definitions:

const PARAMETERS = (; a = 1, b = 2)

function query(; a = 1, b = 2) # you don’t want to repeat yourself here
  a + b
end

One way to do this is like so:

const PARAMETERS = (; a = 1, b = 2)

kws = [Expr(:kw, k, v) for (k, v) in pairs(PARAMETERS)]

@eval function query(; $(kws...))
    a + b
end

You can see the unevaluated code by replacing @eval with quote … end. This requires a little knowledge of how keyword arguments are parsed internally, so I’ll explain how I got there.

If you inspect the structure of an expression with keyword arguments, you’ll see they’re actually Expr(:kw, k, v) objects.

julia> :( f(; a = 1) ) |> dump
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: Symbol f
    2: Expr
      head: Symbol parameters
      args: Array{Any}((1,))
        1: Expr
          head: Symbol kw
          args: Array{Any}((2,))
            1: Symbol a
            2: Int64 1

julia> :( f(; a = 1, b = 2) ).args[2].args # these are the `a = 1, b = 2`
2-element Vector{Any}:
 :($(Expr(:kw, :a, 1)))
 :($(Expr(:kw, :b, 2)))

To properly interpolate (; a = 1, b = 2) into an expression as keyword arguments, we have to convert them into a collection of Expr(:kw, k, v) structs. That is what the kws line above does. Finally, the dot syntax in f(; $(kws...)) is equivalent to f(; $(kws[1]), $(kws[2]), …, $(kws[end])). Hope this helps.

4 Likes

Thanks for the super detailed reply!

@Jollywatt gave a very detailed and accurate answer.

There is an alternative approach, which does not require macros at all. The basic idea is to add two methods, one that handles keyword arguments, and another, that accepts single a named tuple argument. Here’s how:

julia> const PARAMETERS = (a = 1, b = 2)
(a = 1, b = 2)

julia> query(; kwargs...) = query(merge(PARAMETERS, kwargs))
query (generic function with 1 method)

julia> query(params::typeof(PARAMETERS)) = println(params)
query (generic function with 2 methods)

julia> query(a = 4)
(a = 4, b = 2)
1 Like