What are the rules for qualified/unqualified symbols in macros? (or is it a bug?)

Let’s say I want to automate making lots of very similar looking, but semantically distinct types:

macro make_struct(arg)
    quote
        struct $(esc(arg))
            x
        end
        export $(esc(arg))
    end
end

It creates a struct and then exports it. A quick check:

julia> @macroexpand  @make_struct(xxxx)
quote
    #= REPL[1]:3 =#
    struct xxxx
        #= REPL[1]:4 =#
        x
    end
    #= REPL[1]:6 =#
    export xxxx
end

So far so good. I may want to process these structs somehow else. So, let’s put their names into a vector and instantiate all the structs from that list:

const structs_to_make = [:Foo, :Bar]

macro make_many_structs()
    quote
        $([:(@make_struct $(st)) for st in structs_to_make]...)
    end
end

But here $(st) get inserted in the expansion of @make_struct in two different ways depending where it appears:

julia> @macroexpand @make_many_structs
quote
    #= REPL[3]:3 =#
    begin
        #= REPL[1]:3 =#
        struct Main.Foo
            #= REPL[1]:4 =#
            x
        end
        #= REPL[1]:6 =#
        export Foo
    end
    begin
        #= REPL[1]:3 =#
        struct Main.Bar
            #= REPL[1]:4 =#
            x
        end
        #= REPL[1]:6 =#
        export Bar
    end
end

And this will an error in struct definition. If I escape st in @make_many_structs:

macro make_many_structs()
    quote
        $([:(@make_struct $(esc(st))) for st in structs_to_make]...)
    end
end

it doesn’t expand correctly in export statement:

julia> @macroexpand @make_many_structs
quote
    #= REPL[9]:3 =#
    begin
        #= REPL[1]:3 =#
        struct Foo
            #= REPL[1]:4 =#
            x
        end
        #= REPL[1]:6 =#
        export $(Expr(:escape, :Foo))
    end
    begin
        #= REPL[1]:3 =#
        struct Bar
            #= REPL[1]:4 =#
            x
        end
        #= REPL[1]:6 =#
        export $(Expr(:escape, :Bar))
    end
end

Why is it that in one place a symbol gets a qualified name and in another — unqualified? Or is it a bug? Also, any way to workaround?

This may or may not be a bug, I’m not sure - but as a workaround, you can just esc the entire expression returned by your macro and it seems to work.

If you don’t need macro hygiene anywhere, you don’t need to do targeted esc’s like that.

 macro make_struct(arg)
    esc(quote
            struct $arg
                x
            end
            export $arg
        end
    )
end

julia> @macroexpand @make_many_structs
quote
    #= REPL[9]:3 =#
    begin
        #= REPL[6]:3 =#
        struct Foo
            #= REPL[6]:4 =#
            x
        end
        #= REPL[6]:6 =#
        export Foo
    end
    begin
        #= REPL[6]:3 =#
        struct Bar
            #= REPL[6]:4 =#
            x
        end
        #= REPL[6]:6 =#
        export Bar
    end
end

1 Like

Thanks for the tip!

I guess, I am going to one an issue and see what higher powers say :slight_smile:

EDIT
Link to the issue: https://github.com/JuliaLang/julia/issues/40490

This is not a direct answer, but I wanted to try to offer a non-macro solution. And that’s because, while I quite enjoy that Julia has metaprogramming facilities, there do seem to tricky edge cases whenever doing any kind of macro composition.

I would consider making a NamedTuple that has the fields of the struct, and write wrapper types, each with one field containing the NamedTuple values. I think this pattern is a good way to split up naming/semantics from structure, which then allows you to use the structure without resorting to metaprogramming.

I wrote for my own purposes (and with a different motivation) GitHub - goretkin/NamedNamedTuples.jl , and I suggest you look first at https://github.com/BeastyBlacksmith/ProtoStructs.jl if this pattern sounds like it would be useful to you.


That assumes that you don’t have any other reason to need structs_to_make (or even if you do, perhaps you can live with some repetition), and if I’m understanding correctly that you want to minimize repeating the whole definition of a struct, likely with more fields than just x.

Note you won’t have the same expressiveness in constraining the types of the field, but I have often found those constraints to be pesky. e.g., whenever I want a Rational of two different types, say Int64 and a type from StaticNumbers.jl, thereby getting a less feature-full version of `FixedPointNumbers.jl for a lot less code. Those kinds of constraints (and many more) could arguably be better handled by something like “Argument standardization” (how delightful it is to have a name and explanations for this concept).

And, if that particular kind of expressiveness is important, and you are okay with choosing a type name instead of using a NamedTuple (which I like thinking of as an anonymous struct with no identity other than its own structure), then you could use a struct instead of a NamedTuple. Personally, I think the NamedTuple is the right choice because I try (with questionable benefit) to avoid choosing names when it isn’t too inconvenient. Even the field name of the single field inside the wrapper types, I would name _. It is only legal when there’s a single field and so it seems least arbitrary name.

I am failing to follow how this will help…

You want to avoid this kind of duplication

struct Foo1{A, B, C, D}
    a::A
    b::B
    c::C
    d::D
end

struct Foo2{A, B, C, D}
    a::A
    b::B
    c::C
    d::D
end

struct Foo3{A, B, C, D}
    a::A
    b::B
    c::C
    d::D
end
# [...]

right?

What I’m suggesting is basically

# common_structure{A,B,C,D} = NamedTuple{(:a, :b, :c, :d), Tuple{A, B, C, D}}
common_structure = NamedTuple{(:a, :b, :c, :d)}

struct Foo1{T <: common_structure}
    _::T
end

struct Foo2{T <: common_structure}
    _::T
end

struct Foo3{T <: common_structure}
    _::T
end

That alone gives

julia> foo1 = Foo1(common_structure((1,2,3,4)))
Foo1{NamedTuple{(:a, :b, :c, :d), NTuple{4, Int64}}}((a = 1, b = 2, c = 3, d = 4))

julia> foo1 = Foo1((a=1, b=2, c=3, d=4)) # equivalent
Foo1{NamedTuple{(:a, :b, :c, :d), NTuple{4, Int64}}}((a = 1, b = 2, c = 3, d = 4))

julia> foo1.a
ERROR: type Foo1 has no field a
Stacktrace:
 [1] getproperty(x::Foo1{NamedTuple{(:a, :b, :c, :d), NTuple{4, Int64}}}, f::Symbol)
   @ Base ./Base.jl:33
 [2] top-level scope
   @ REPL[23]:1

julia> foo1._.a
1

If you want to access the fields directly, instead of via _, then you can define getproperty appropriately. You might also want a constructor like Foo1(a,b,c,d). The packages I mentioned should help with that. They do, of course, use metaprogramming for those definitions. But I like that the structure is specified as a NamedTuple, instead of in an expression.