Strategy to manage two similar macros which define functions

I have two long macros that are almost equal, apart from a few lines:

macro m1(#= args of m1... =#)
    # set some variables in m1 and m2 before returning the expression...
    # define some other variables only in m1...
    quote
        # first part of the body in common to m1 and m2...
        # this is only in m1...
        # second part in common to m1 and m2...
    end
end

macro m2(#= args of m2... =#)
    # set some variables in m1 and m2 before returning the expression...
    quote
        # first part of the body in common to m1 and m2...
        # second part in common to m1 and m2...
    end
end

What’s the best strategy to manage this code and reduce code duplication? I guess I should define functions working on expressions, but I don’t really know how to do that in practice, also because given how @m1 is defined I’d need to concatenate multiple Expr and I have no idea how this works.

For the record, the real code I’d like to simplify and make more maintainable is at PhysicalConstants.jl/PhysicalConstants.jl at 144d5dcc75f213a4c28c3c67148ac3632cf8c766 · JuliaPhysics/PhysicalConstants.jl · GitHub

Do you mean something like this?

julia> e1 = :(a=1);
julia> e2 = :(b=2);
julia> quote
           $e1
           $e2
       end
quote
    #= REPL[7]:2 =#
    a = 1
    #= REPL[7]:3 =#
    b = 2
end

So concatenating expressions is just putting expressions within a quote environment, this is great.

I thought I tried something like previously, but apparently I didn’t.

Well, I’m pretty sure that I did try. In fact now I’m struggling again with the same issue I had before: function definitions (now I remember).

Basically, I have something like this:

function foo end

macro m1(val)
    eval = esc(val)
    quote
        Main.foo() = $eval
    end
end

and I can do

julia> @m1 5

julia> foo()
5

julia> @m1 4.9

julia> foo()
4.9

However, if I try to move the returned expression of the macro to a function, it doesn’t work any more:

function foo end

function _m2(eval)
    quote
        Main.foo() = $eval
    end
end

macro m2(val)
    eval = esc(val)
    quote
        _m2($eval)
    end
end

in the REPL:

julia> include("/tmp/foo.jl")
@m2 (macro with 1 method)

julia> @m2 5
quote
    #= /tmp/foo.jl:5 =#
    Main.foo() = begin
            #= /tmp/foo.jl:5 =#
            5
        end
end

julia> foo()
ERROR: MethodError: no method matching foo()
Stacktrace:
 [1] top-level scope at none:0

Your _m2 function already returns an expression, and the macro unnecessarily quotes it into another expression. You just need

macro m2(val)
    _m2(esc(val))
end

If you want to concatenate two such expressions, you can do somthing like

macro m2(val)
    quote
        $(_m1(esc(val)))
        $(_m2(esc(val)))
    end
end
2 Likes

Right, this makes sense and works, thank you so much!

I’m almost done. If I’d like to move also the part before the returned expression to a function, I’d have to do that? Should the function return an expression that has to be @evaled in the macro?

No, you should never need to call @eval or eval() within a macro. Your macro should just call the function returning an expression, no further action needed.

For example:

julia> function make_expr()
         return :(2 + 2)
       end
make_expr (generic function with 1 method)

julia> macro foo()
         return make_expr()
       end
@foo (macro with 1 method)

julia> @macroexpand @foo()
:(2 + 2)

julia> @foo()
4
1 Like

I asked about what is before the returned expression, the “preamble” (is there a specific term to identify this part of the macro?), where I set some variables that will be used within the returned expression.

In this part you could use “standard” (i.e. not macro-specific) code factoring techniques : regular functions taking some arguments and returning the set of values which you want to use in the remaining part of your macro.

Maybe something like this ?

common_vars() = (1, 2)
m1_vars() = 3

common_code(v1, v2, e) = quote
    println($v1, $v2, $e)
end

m1_code(v1, v2, v3, e) = quote
    sum(($v1, $v2, $v3, $e))
end

macro m1(expr)
    var1, var2 = common_vars()
    var3 = m1_vars()
    quote
        $(common_code(var1, var2, esc(expr)))
        $(m1_code(var1, var2, var3, esc(expr)))
    end
end
julia> @macroexpand @m1 x
quote
    #= REPL[5]:5 =#
    begin
        #= REPL[3]:2 =#
        (Main.println)(1, 2, x)
    end
    #= REPL[5]:6 =#
    begin
        #= REPL[4]:2 =#
        (Main.sum)((1, 2, 3, x))
    end
end

julia> let x = 4
           @m1 x
       end
124
10
1 Like

Well, that was super easy, shame on me :frowning: Thanks a lot!

For reference, I’ve implemented the suggestions in this thread in this PR: Improve maintainability of macros by giordano · Pull Request #8 · JuliaPhysics/PhysicalConstants.jl · GitHub