Julia Language Metaprogramming QuoteNode, Meta.quot, and Expr(:quote)


Example

There are three ways to quote something using a Julia function:

julia> QuoteNode(:x)
:(:x)

julia> Meta.quot(:x)
:(:x)

julia> Expr(:quote, :x)
:(:x)

What does "quoting" mean, and what is it good for? Quoting allows us to protect expressions from being interpreted as special forms by Julia. A common use case is when we generate expressions that should contain things that evaluate to symbols. (For example, this macro needs to return a expression that evaluates to a symbol.) It doesn't work simply to return the symbol:

julia> macro mysym(); :x; end
@mysym (macro with 1 method)

julia> @mysym
ERROR: UndefVarError: x not defined

julia> macroexpand(:(@mysym))
:x

What's going on here? @mysym expands to :x, which as an expression becomes interpreted as the variable x. But nothing has been assigned to x yet, so we get an x not defined error.

To get around this, we must quote the result of our macro:

julia> macro mysym2(); Meta.quot(:x); end
@mysym2 (macro with 1 method)

julia> @mysym2
:x

julia> macroexpand(:(@mysym2))
:(:x)

Here, we have used the Meta.quot function to turn our symbol into a quoted symbol, which is the result we want.

What is the difference between Meta.quot and QuoteNode, and which should I use? In almost all cases, the difference does not really matter. It is perhaps a little safer sometimes to use QuoteNode instead of Meta.quot. Exploring the difference is informative into how Julia expressions and macros work, however.

The difference between Meta.quot and QuoteNode, explained

Here's a rule of thumb:

  • If you need or want to support interpolation, use Meta.quot;
  • If you can't or don't want to allow interpolation, use QuoteNode.

In short, the difference is that Meta.quot allows interpolation within the quoted thing, while QuoteNode protects its argument from any interpolation. To understand interpolation, it is important to mention the $ expression. There is a kind of expression in Julia called a $ expression. These expressions allow for escaping. For instance, consider the following expression:

julia> ex = :( x = 1; :($x + $x) )
quote 
    x = 1
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

When evaluated, this expression will evaluate 1 and assign it to x, then construct an expression of the form _ + _ where the _ will be replaced by the value of x. Thus, the result of this should be the expression 1 + 1 (which is not yet evaluated, and so distinct from the value 2). Indeed, this is the case:

julia> eval(ex)
:(1 + 1)

Let's say now that we're writing a macro to build these kinds of expressions. Our macro will take an argument, which will replace the 1 in the ex above. This argument can be any expression, of course. Here is something that is not quite what we want:

julia> macro makeex(arg)
           quote
               :( x = $(esc($arg)); :($x + $x) )
           end
       end
@makeex (macro with 1 method)

julia> @makeex 1
quote 
    x = $(Expr(:escape, 1))
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

julia> @makeex 1 + 1
quote 
    x = $(Expr(:escape, 2))
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

The second case is incorrect, because we ought to keep 1 + 1 unevaluated. We fix that by quoting the argument with Meta.quot:

julia> macro makeex2(arg)
           quote
               :( x = $$(Meta.quot(arg)); :($x + $x) )
           end
       end
@makeex2 (macro with 1 method)

julia> @makeex2 1 + 1
quote 
    x = 1 + 1
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

Macro hygiene does not apply to the contents of a quote, so escaping is not necessary in this case (and in fact not legal) in this case.

As mentioned earlier, Meta.quot allows interpolation. So let's try that out:

julia> @makeex2 1 + $(sin(1))
quote 
    x = 1 + 0.8414709848078965
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

julia> let q = 0.5
           @makeex2 1 + $q
       end
quote 
    x = 1 + 0.5
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

From the first example, we see that interpolation allows us to inline the sin(1), instead of having the expression be a literal sin(1). The second example shows that this interpolation is done in the macro invocation scope, not the macro's own scope. That's because our macro hasn't actually evaluated any code; all it's doing is generating code. The evaluation of the code (which makes its way into the expression) is done when the expression the macro generates is actually run.

What if we had used QuoteNode instead? As you may guess, since QuoteNode prevents interpolation from happening at all, this means it won't work.

julia> macro makeex3(arg)
           quote
               :( x = $$(QuoteNode(arg)); :($x + $x) )
           end
       end
@makeex3 (macro with 1 method)

julia> @makeex3 1 + $(sin(1))
quote 
    x = 1 + $(Expr(:$, :(sin(1))))
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

julia> let q = 0.5
           @makeex3 1 + $q
       end
quote 
    x = 1 + $(Expr(:$, :q))
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

julia> eval(@makeex3 $(sin(1)))
ERROR: unsupported or misplaced expression $
 in eval(::Module, ::Any) at ./boot.jl:234
 in eval(::Any) at ./boot.jl:233

In this example, we might agree that Meta.quot gives greater flexibility, as it allows interpolation. So why might we ever consider using QuoteNode? In some cases, we may not actually desire interpolation, and actually want the literal $ expression. When would that be desirable? Let's consider a generalization of @makeex where we can pass additional arguments determining what comes to the left and right of the + sign:

julia> macro makeex4(expr, left, right)
           quote
               quote
                   $$(Meta.quot(expr))
                   :($$$(Meta.quot(left)) + $$$(Meta.quot(right)))
               end
           end
       end
@makeex4 (macro with 1 method)

julia> @makeex4 x=1 x x
quote  # REPL[110], line 4:
    x = 1 # REPL[110], line 5:
    $(Expr(:quote, :($(Expr(:$, :x)) + $(Expr(:$, :x)))))
end

julia> eval(ans)
:(1 + 1)

A limitation of our implementation of @makeex4 is that we can't use expressions as either the left and right sides of the expression directly, because they get interpolated. In other words, the expressions may get evaluated for interpolation, but we might want them preserved. (Since there are many levels of quoting and evaluation here, let us clarify: our macro generates code that constructs an expression that when evaluated produces another expression. Phew!)

julia> @makeex4 x=1 x/2 x
quote  # REPL[110], line 4:
    x = 1 # REPL[110], line 5:
    $(Expr(:quote, :($(Expr(:$, :(x / 2))) + $(Expr(:$, :x)))))
end

julia> eval(ans)
:(0.5 + 1)

We ought to allow the user to specify when interpolation is to happen, and when it shouldn't. Theoretically, that's an easy fix: we can just remove one of the $ signs in our application, and let the user contribute their own. What this means is that we interpolate a quoted version of the expression entered by the user (which we've already quoted and interpolated once). This leads to the following code, which can be a little confusing at first, due to the multiple nested levels of quoting and unquoting. Try to read and understand what each escape is for.

julia> macro makeex5(expr, left, right)
           quote
               quote
                   $$(Meta.quot(expr))
                   :($$(Meta.quot($(Meta.quot(left)))) + $$(Meta.quot($(Meta.quot(right)))))
               end
           end
       end
@makeex5 (macro with 1 method)

julia> @makeex5 x=1 1/2 1/4
quote  # REPL[121], line 4:
    x = 1 # REPL[121], line 5:
    $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 / 2)))))) + $(Expr(:$, :($(Expr(:quote, :(1 / 4)))))))))
end

julia> eval(ans)
:(1 / 2 + 1 / 4)

julia> @makeex5 y=1 $y $y
ERROR: UndefVarError: y not defined

Things started well, but something has gone wrong. The macro's generated code is trying to interpolate the copy of y in the macro invocation scope; but there is no copy of y in the macro invocation scope. Our error is allowing interpolation with the second and third arguments in the macro. To fix this error, we must use QuoteNode.

julia> macro makeex6(expr, left, right)
           quote
               quote
                   $$(Meta.quot(expr))
                   :($$(Meta.quot($(QuoteNode(left)))) + $$(Meta.quot($(QuoteNode(right)))))
               end
           end
       end
@makeex6 (macro with 1 method)

julia> @makeex6 y=1 1/2 1/4
quote  # REPL[129], line 4:
    y = 1 # REPL[129], line 5:
    $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 / 2)))))) + $(Expr(:$, :($(Expr(:quote, :(1 / 4)))))))))
end

julia> eval(ans)
:(1 / 2 + 1 / 4)

julia> @makeex6 y=1 $y $y
quote  # REPL[129], line 4:
    y = 1 # REPL[129], line 5:
    $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
end

julia> eval(ans)
:(1 + 1)

julia> @makeex6 y=1 1+$y $y
quote  # REPL[129], line 4:
    y = 1 # REPL[129], line 5:
    $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :(1 + $(Expr(:$, :y)))))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
end

julia> @makeex6 y=1 $y/2 $y
quote  # REPL[129], line 4:
    y = 1 # REPL[129], line 5:
    $(Expr(:quote, :($(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)) / 2)))))) + $(Expr(:$, :($(Expr(:quote, :($(Expr(:$, :y)))))))))))
end

julia> eval(ans)
:(1 / 2 + 1)

By using QuoteNode, we have protected our arguments from interpolation. Since QuoteNode only has the effect of additional protections, it is never harmful to use QuoteNode, unless you desire interpolation. However, understanding the difference makes it possible to understand where and why Meta.quot could be a better choice.

This long exercise is with an example that is plainly too complex to show up in any reasonable application. Therefore, we have justified the following rule of thumb, mentioned earlier:

  • If you need or want to support interpolation, use Meta.quot;
  • If you can't or don't want to allow interpolation, use QuoteNode.

What about Expr(:quote)?

Expr(:quote, x) is equivalent to Meta.quot(x). However, the latter is more idiomatic and is preferred. For code that heavily uses metaprogramming, a using Base.Meta line is often used, which allows Meta.quot to be referred to as simply quot.