Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing a macro that returns multiple toplevel expressions in Julia

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.

like image 603
MPaga Avatar asked Mar 10 '20 20:03

MPaga


1 Answers

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
like image 141
François Févotte Avatar answered Nov 13 '22 19:11

François Févotte