Jordi Manyer Fuertes
Jordi Manyer Fuertes

Reputation: 149

Defining Julia types using macros

Let's say I want to define a series of structs that will be used as parametric types for some other struct later on. For instance, I would like to have something like

abstract type Letter end

struct A <: Letter end
struct B <: Letter end
...etc...

The idea I've had is to define a macro which takes a string of the name of the struct I want to create and defines it as well as some basic methods. I would then execute the macro in a loop for all names and get all my structs defined at compile time. Something like this:

const LETTERS = ["A","B","C","D"]
abstract type Letter end

macro _define_type(ex)
    lines = Vector{Expr}()
    push!(lines, Meta.parse("export $ex"))
    push!(lines, Meta.parse("struct $ex <: Letter end"))
    push!(lines, Meta.parse("string(::Type{$ex}) = \"$ex\""))
    return Expr(:block,lines...)
end

@_define_type "A"

for ex in LETTERS
    @_define_type ex
end

The first way of executing the macro (with a single string) works and does what I want. However, when I execute the macro in a loop it does not. It tells me some variables are declared both as local and global variables.

Can someone explain what is happening? I believe it may be solved by a proper use of esc, but I can't figure out how.

Thanks!

Edit: Thank you all for the amazing responses! Got my code running!

Upvotes: 1

Views: 418

Answers (3)

ahnlabb
ahnlabb

Reputation: 2162

While there are other ways of achieving what you want to do I believe the core issue is understanding the nature of macros. From the docs (emphasis mine):

Macros are necessary because they execute when code is parsed, therefore, macros allow the programmer to generate and include fragments of customized code before the full program is run.

So the macro in your loop does not "see" the values "A", "B", "C" and "D", it sees the expression: ex. To demonstrate this try using @macroexpand:

julia> @macroexpand @_define_type ex
quote
    export ex
    struct ex <: Main.Letter
        #= none:1 =#
    end
    var"#11#string"(::Main.Type{Main.ex}) = begin
            #= none:1 =#
            "ex"
        end
end

As you can see the actual value of the variable ex does not matter. With this in mind let's look at the actual error you get. You can reproduce it like this:

julia> for ex in ["A"]
           struct ex <: Letter
           end
       end
ERROR: syntax: variable "ex" declared both local and global
Stacktrace:
 [1] top-level scope
   @ REPL[52]:1

You can probably see that this is not what you want, but why this specific error? The reason is that structs are implicitly global while the loop variable is local.


Here is a possible solution that uses a macro that takes a variable number of arguments instead (I also switched to providing the expression directly instead of as a string):

abstract type Letter end

macro _define_types(exprs...)
    blocks = map(exprs) do ex
        name = string(ex)
        quote
            export $ex
            struct $ex <: Letter end
            Base.string(::Type{$ex}) = $name
        end
    end
    Expr(:block, blocks...)
end

@_define_types A

@_define_types B C D 

Upvotes: 1

Elias Carvalho
Elias Carvalho

Reputation: 296

I think this is what you are trying to do:

module MyModule

abstract type Letter end
    
const LETTERS = ["A", "B", "C", "D"]

for letter in LETTERS
    sym = Symbol(letter)
    @eval begin
        export $sym

        struct $sym <: Letter end
        Base.string(::Type{$sym}) = $letter
    end
end

end

using .MyModule

string(A) # "A"

See the Code Generation section for more details: https://docs.julialang.org/en/v1/manual/metaprogramming/#Code-Generation

Upvotes: 3

Tsela
Tsela

Reputation: 244

Okay, the problem here is that in Julia for loops introduce a separate, local scope, while struct definitions need to be in the global scope. So your macro fails because it creates struct definitions in the local scope of the for loop.

A way to get around this is to use @eval, to ensure your struct definitions are evaluated in the global scope. In that case, you don't need to create a macro for that, just have a simple loop like this:

abstract type Letter end

const LETTERS = [:A, :B, :C, :D, :E]

for ex in LETTERS
  @eval struct $ex <: Letters end
end

You can even put that loop in a function and it will still work. The defined structs can have fields, as @eval covers the entire code block that follows it.

Note that LETTERS must contain symbols rather than strings for this to work correctly. It's easy enough to convert a vector of strings into a vector of symbols using Symbol.(vector_of_strings).

Upvotes: 1

Related Questions