Macro question (create variables)

Wanted: a macro

@moo a b

which is in effect equivalent to

a = foo("a")
b = foo("b")

and actually I got it:

foo(x) = "$x-$x" 

macro moo(args...)
    for x in args
        xs = string(x)
        @eval eval(Meta.parse("$($xs) = foo(\"$($xs)\")"))
    end
end

@moo v1 v2
@show v1 v2 ;

julia> 

v1 = "v1-v1"
v2 = "v2-v2"

However @eval eval(Meta.parse( looks ugly to me, there is probably a more proper way to achieve the result.

Don’t use eval within a macro. A macro is a function that get’s a parsed, yet unevaluated, expression as input and can transform it into another expression which is to be executed instead.
More concretely, let’s write @moo with a single variable as input:

  1. What input does @moo get when called as @moo a?
julia> expr = Meta.parse("a")  # argument expression after being parsed
:a

julia> typeof(expr)
Symbol

julia> dump(expr)  # expression, i.e., input to @moo will just be a symbol
Symbol a

From this symbol, we now want to construct a more complicated expression which contains the literal symbol as well as its name as a string

julia> res_expr = Meta.parse("a = foo(\"a\")")
:(a = foo("a"))  # desired output of @moo a

julia> dump(res_expr)
Expr
  head: Symbol =
  args: Array{Any}((2,))
    1: Symbol a
    2: Expr
      head: Symbol call
      args: Array{Any}((2,))
        1: Symbol foo
        2: String "a"

Now, the macro is a pure function which takes the symbol as input and produces the desired expression as output:

julia> macro moo(var)
           # Return expression with symbol interpolated literally on the lhs and as a string on the rhs
           :($(esc(var)) = foo($(string(var))))
       end
@moo (macro with 1 method)

julia> @macroexpand @moo a  # Macro indeed transforms the given symbol into the desired expression
:(a = Main.foo("a"))

Be sure to also read the Metaprogramming chapter from the Julia manual. Other good sources to understand macros in general, i.e., not just in Julia, are On Lisp or Practical Common Lisp.

7 Likes

bertschi thank you for the detailed explanation.

After some searching (specifically this and this topics were useful) and some experimenting I got solution for multiple arguments:

foo(x) = "$x-$x" 

macro moo2(args...) 
    v = [:($x = foo($(string(x)))) for x in args]
    return esc(Expr(:block, v...))
end

@moo2 w1 w2
@show w1 w2;

julia> 

w1 = "w1-w1"
w2 = "w2-w2"

Now, can anybody explain me why do I need this esc here in the return statement?

This comes down to what we call “macro hygeine”. There’s a section in the documentation here: Metaprogramming · The Julia Language

The basic idea is that if you write something like

macro foo(ex)
    quote
        x = 1
        y = x + $ex
     end
end 

it would be very bad and surprising if the variables referred to there affected code outside of the macro unless you specifically intend it to. E.g. consider this:

function f(x)
    @foo x
    x
end

Without macro hygiene, f(2) would return 1, which might be very surprising. So instead what happens is this:

julia> @macroexpand @foo ex
quote
    #= REPL[17]:3 =#
    var"#59#x" = 1
    #= REPL[17]:4 =#
    var"#60#y" = var"#59#x" + Main.ex
end

So the intermediate variables here aren’t actually named x or y, they get names that are guaranteed to not clash with surrounding code.

However, sometimes you don’t want this, especialy if you need to actually return and evaluate code that a user passed into the macro. So when you write esc(expr), that essentially is a marker saying "I want everything inside expr to not be made hygenic.

1 Like

The best practice is to only apply esc to specific parts of the returned expression which need it. So in your case, I would write your macro as

macro moo3(args...) 
    v = [:($(esc(x)) = foo($(string(x)))) for x in args]
    return Expr(:block, v...)
end

because then foo does not get escaped.

Here’s a demonstraction of why:

julia> let 
           foo(x) = x #some user locally redefines `foo(x)`
           @moo2 w1 w2
           w1, w2
       end 
("w1", "w2") 

uh-oh, this isn’t the output we intended!

Now look at how @moo3 deals with this:

julia> let 
           foo(x) = x
           @moo3 w1 w2
           w1, w2
       end
("w1-w1", "w2-w2")
2 Likes