Writing a macro that returns multiple toplevel expressions in Julia
Asked Answered
A

1

6

I am trying to write a macro that defines multiple methods for a type hierarchy. What I am trying to achieve is a way to arbitrarely enumerate a type hierarchy, by defining an order() method for each struct in the type tree.

macro enum_type(type)
    let type = eval(type)
        next = [type]
        methods = []
        counter = 0
        while(!isempty(next))
            let current_type = pop!(next)
                children = subtypes(current_type)
                map(t -> push!(next, t), children)
                push!(methods, :(order(::$current_type) = $(counter += 1)))
            end
        end
        quote
            $(methods...)
        end
    end
end

The returned expressions do not seem to be evaluated in toplevel. Is there a way to return multiple toplevel expressions?

The desired behaviour would be to create a method for each type in the hierarchy, as an example, consider

@macroexpand @enum_type (AbstractFloat)

Should write a method order(...) associating an arbitrary number to each type in the type tree starting from AbstractFloat. For now, the expansion of the macro with argument AbstractFloat is

quote
    #= none:14 =#
    var"#57#order"(::AbstractFloat) = begin
            #= none:10 =#
            1
        end
    var"#57#order"(::Float64) = begin
            #= none:10 =#
            2
        end
    var"#57#order"(::Float32) = begin
            #= none:10 =#
            3
        end
    var"#57#order"(::Float16) = begin
            #= none:10 =#
            4
        end
    var"#57#order"(::BigFloat) = begin
            #= none:10 =#
            5
        end
end

But none of the method declaration are being evaluated.

Arundinaceous answered 10/3, 2020 at 20:12 Comment(4)
Can you add a MWE showing why they are not top-level? (I mean they aren't, but in this case that should not be a problem from what I see. You are not define anything that is top-level only)Papandreou
FYI, your @enum macro would shadow the @enum macro from Base.Plum
To echo @LyndonWhite, can you add an example that shows the desired behavior?Plum
added some more detailsArundinaceous
S
5

It looks like your problem is not related to the generated expressions being top-level or not. It is rather related to the fact that you'd like to define a generic function named order (and multiple methods associated to it), but the name order itself is not preserved in the macro expansion: as you can see in the macro expansion you posted, order has been replaced with var"#57order", which is a name that no user-defined function can actually have. This has been done to help tackle a common issue with macros, which is called hygiene.

I think the smallest modification you could make to have a macro behaving like you want would be to escape the function name (order) in the generated expression, so that the name remains untouched:

macro enum_type(type)
    let type = eval(type)
        next = [type]
        methods = []
        counter = 0
        while(!isempty(next))
            let current_type = pop!(next)
                children = subtypes(current_type)
                map(t -> push!(next, t), children)
                # see how esc is used to prevent the method name `order` from
                # being "gensymmed"
                push!(methods, :($(esc(:order))(::$current_type) = $(counter += 1)))
            end
        end
        quote
            $(methods...)
        end
    end
end

IIUC, this does what you want (and method definitions are still not top-level):

julia> @macroexpand @enum_type AbstractFloat
quote
    #= REPL[1]:14 =#
    order(::AbstractFloat) = begin
            #= REPL[1]:10 =#
            1
        end
    order(::Float64) = begin
            #= REPL[1]:10 =#
            2
        end
    order(::Float32) = begin
            #= REPL[1]:10 =#
            3
        end
    order(::Float16) = begin
            #= REPL[1]:10 =#
            4
        end
    order(::BigFloat) = begin
            #= REPL[1]:10 =#
            5
        end
end

julia> @enum_type AbstractFloat
order (generic function with 5 methods)

julia> order(3.14)
2




Now, there are other things that come to mind reading your macro:

  • it is generally frowned upon to use eval within the body of a macro

  • your let blocks are not really needed here; I guess it would be more idiomatic to leave them out

  • map(f, xs) produces an array containing all values [f(x) for x in xs]. If f is only used for its side effects, foreach should be used instead. (EDIT: as noted by @CameronBieganek, append! does precisely what's needed in this specific case)

  • I think that for such a task, where metaprogramming is used to generate top-level code out of (almost) nothing, rather than transforming an (user-provided) expression (possibly inside a given scope/context), I would use @eval rather than a macro.

So I would perhaps use code such as the following:

function enum_type(type)
    next = [type]
    counter = 0
    while(!isempty(next))
        current_type = pop!(next)
        append!(next, subtypes(current_type))

        @eval order(::$current_type) = $(counter += 1)
    end
end

which produces the same results:

julia> enum_type(AbstractFloat)

julia> order(3.14)
2
Spermato answered 11/3, 2020 at 20:50 Comment(2)
Even simpler than foreach would be append!(next, subtypes(current_type)).Plum
I edited the post to include this simplification. Thanks!Sky

© 2022 - 2024 — McMap. All rights reserved.