How to construct `Expr(:tuple, ...)` using templates

I tried to make an argument list for a closure from a tuple of symbols. MWE:

## what I want
macro_helper1(arguments, form) = :($(Expr(:tuple, arguments...)) -> $form)
## how I tried to do it first
macro_helper2(arguments, form) = :($arguments -> $form)

difference:

julia>  macro_helper1((gensym(:a),gensym(:b)), :(f(something)))  # good solution
:((##a#321,##b#322)->begin  # REPL[168], line 1:
            f(something)
        end)

julia>  macro_helper2((gensym(:a),gensym(:b)), :(f(something)))  # wrong solution
:((Symbol("##a#323"),Symbol("##b#324"))->begin  # REPL[170], line 1:
            f(something)
        end)

I figured out the Expr(:tuple,...) using dump.

  1. Is this the expected behavior?
  2. Could someone please explain why? (I would learn a lot from it).
  3. How to do this without Expr?

When evaluating an expression tree, the only thing that has any meaning is an Expr object (to a first approximation at least). Everything else is treated as a black box by the compiler and simply left alone. For example:

type Foo
  x
end

eval(Expr(:call, :+, :a, Foo(:(b+c))))
# which is equivalent to
a + Foo(:(b+c))
# NOT to
a + Foo(b + c)

If you evaluate this expression, b+c will never be evaluated, because a Foo has to be left as-is. In your case you’re working with a Tuple rather than a Foo but the principle is exactly the same.

So in your example, what’s happening is that the compiler looks for a set of names to serve as the argument list, but it only finds a value it isn’t allowed to look at – since it can only look inside an Expr(:tuple).

In theory there could be rules for how tuples (or arbitrary objects) are interpreted as expressions so that there’s effectively no difference between :(a, b) and (:a, :b). This would make your example behave more intuitively, but would also make the boundary between code and data in Expr objects much more complex.

Tuple expressions can be constructed in the normal way by quoting them: :(a, b), :(foo((1,2))) etc. And you can splice into them as usual as well; the trick is to start with a one-element tuple expression :(a,) and unquote-splat :($(xs...),), making the final version:

macro_helper1(arguments, form) = :(($(arguments...),) -> $form)
1 Like

Thanks, now I get how everything but Expr is untouched. It not the level of homoiconicity Lisp has, but it is consistent once one knows the rule.

I think I also understand the tuple construction, mostly. If I dropped the , and had

:(($(arguments...)) -> $form)

then it would be equivalent to

:($(arguments...) -> $form)

which makes

Expr
  head: Symbol ->
  args: Array{Any}((3,))
    1: Symbol a
    2: Symbol b
    3: Expr
      head: Symbol block
      args: Array{Any}((2,))
        1: Expr
          head: Symbol line
          args: Array{Any}((2,))
            1: Int64 1
            2: Symbol REPL[190]
          typ: Any
        2: Symbol form
      typ: Any
  typ: Any

which somehow splices as a -> with 3 args, which is invalid. It is curious that one can do this (but apparently validation does not happen before evaluation). So I need to tell Julia that I want a tuple, hence the ,. Is this correct?

Yup, that’s it.

:($a -> $b) == Expr(:->, a, b)
:($(a...) -> $b) == Expr(:->, a..., b)
:($x,) == Expr(:tuple, x)
:($(x...),) == Expr(:tuple, x...)

Unfortunately the abstraction isn’t 100% waterproof. There may be an argument for special casing how splats at certain locations are handled, but I’m not sure whether it’d be worth the loss of consistency. OTOH it lets you do some fun things like:

x = [:f, 1, 2]
:($(x...)()) == :(f(1,2))

(which will make sense if you expand it like the above).

2 Likes