How to understand expression interpolation

Look at the following two code snippets.

module Foo

export @bar

struct foo
end

macro bar(ex)
    temp = foo
    eval(current_module(), :($temp))
    return esc(ex)
end

end

using Foo

@bar :(Here)
module Foo

export @bar

struct foo
end

macro bar(ex)
    eval(current_module(), :(foo))
    return esc(ex)
end

end

using Foo

@bar :(Here)

In my understanding, the two snippets are effectively the same, since interpolation is basically substitution. However, the first snippet works as expected while the second complains “LoadError: UndefVarError: foo not defined”.

What is happening here?

macro bar(ex)
    temp = foo       # expands to Foo.foo
    eval(current_module(), :($temp))
    return esc(ex)
end
macro bar(ex)    
    eval(current_module(), :(foo))   # still just `foo` which is undefined in Main
    return esc(ex)
end

I believe it’s part of macro hygiene.

Also I understand that it’s just a toy example, but I’m still suspicious about using eval() inside of a macro.

1 Like

I am stilled confused. In my understanding, macro hygiene deals with expressions returned by macro, not the body of macro. Do I misunderstand something?

It might not be logically a part of hygiene features, but it’s a related one. The point is that macros in Julia are a bit more complicated than just substituting some text. In your first example:

macro bar(ex)
    temp = foo
    eval(current_module(), :($temp))
    return esc(ex)
end

Julia knows that the macro is defined in module Foo, so foo refers to Foo.foo. Thus when you use this macro in another module, during macroexpand time Julia assigns temp = Foo.foo and then evaluates :($temp) in the context of that module:

macro bar(ex)
    temp = foo
    x = eval(current_module(), :($temp))
    println(x)
    return esc(ex)
end
...
macroexpand(:(@bar :(Here)))
# prints "Foo.foo"

Note how Julia conveniently found the correct object foo from the scope of macro definition. It’s very similar to example with time from hygiene section of manual I pointed to, even though time is used in a returned expression while foo is only used in body of the macro.

In your second example you don’t actually have any references to Foo.foo, but just a symbol :foo. For Julia symbol :foo is just data, so it’s passed as is. During macroexpand time when you call eval(current_module(), :(foo)) Julia tries to find a thing called foo in the current module (Main), but it’s not defined:

macro bar(ex)    
    x = eval(current_module(), :(foo))
    println(x)
    return esc(ex)
end
...
@bar :(Here)
# ERROR: UndefVarError: foo not defined

Though, if you define foo in Main, it will work fine:

foo = "macroprogramming is awesome!"
@bar :(Here)
# prints "macroprogramming is awesome!"
2 Likes

Thank you.

Is this one of the small tricks that julia does or is there a complete theory of interpolation? If the latter is true, any good references?

This implementation is bound to Julia module and compilation systems and thus is different from other implementations, but in general the question of how (not) to mix things from macro definition and macro expansion scope, as well as macro definition, macro expansion time and run time are well-known. For example, in Common Lisp there’s no macro hygiene - you get (almost) what you have written in a returned expression. But it brings a problem of capturing enclosing variables, so Common Lispers have to use (gensym) - a function generating unique names - to protect expanded code. Yet in practice it’s quite error-prone, so many languages, including Julia, moved to hygienic approach, generating unique names by default:

macro baz(x)
    return quote
        y = 2x
        z = y + 1
        print(z)
    end
end

macroexpand(:(@baz 42))

which produces (note intermediate variable names):

quote  # none, line 3:
    #3#y = 2 * Main.x # none, line 4:
    #4#z = #3#y + 1 # none, line 5:
    (Main.print)(#4#z)
end

Now consider yet another example:

module Foo

macro my_macro()
    print(foo)
end

function my_func()
    print(foo)
end

end

# Main
import Foo

struct foo
end

Foo.my_func()
Foo.@my_macro()

As mentioned by someone else here, macros in Julia are just functions with funny names. And as functions, they obey normal rules of name resolution: although macro my_macro is expanded in Main, it’s defined in Foo, just like function my_func.

As expected, In both cases Julia tries to resolve the name foo in the context of Foo and, as expected, it fails for both - function and macro calls.

2 Likes

Thank you.