Macro to automatically create large number of variables

macros

#1

Hello everyone,

I have a model where I need to generate a large number of variables of the form

    r       = exp.(Xss[indexes.rSS] .+ X[indexes.r])
    rPrime  = exp.(Xss[indexes.rSS] .+ XPrime[indexes.r])
    w       = exp.(Xss[indexes.wSS] .+ X[indexes.w])
    wPrime  = exp.(Xss[indexes.wSS] .+ XPrime[indexes.w])

within a function and and I’d like to use metaprogramming to not have to do this manually.

At the global scope, I can easily do this with eval()

# Preliminary setup
struct index
    r::Int
    w::Int
    rSS::Int
    wSS::Int
end
indexes = index(1,2,1,2)
X = [1.0 2.0]
XSS = [3.0 4.0]
XPrime = [5.0 6.0]
var_names = (:r, :w)

# Create equations
for i in var_names
    varnamePrime = Symbol(i, "Prime")
    varnameSS    = Symbol(i, "SS")
    ex = quote
        $i = exp.(XSS[indexes.$varnameSS] + X[indexes.$i])
        $varnamePrime = exp.(XSS[indexes.$varnameSS] + XPrime[indexes.$i])
    end
    eval(ex)
end

and it also works with a macro at global scope

macro gen_equations()
    ex = quote # initialize expression to append to
        temp = 1
    end

    for i in var_names # loop over variables to generate
        varnamePrime = Symbol(i, "Prime")
        varnameSS = Symbol(i, "SS")
        ex_aux = quote
            $i = exp.(XSS[indexes.$varnameSS] + X[indexes.$i])
            $varnamePrime = exp.(XSS[indexes.$varnameSS] + XPrime[indexes.$i])
        end

        append!(ex.args, ex_aux.args) # append to expression
    end

    return esc(ex)
end

@gen_equations

However, if I put everything in a function

function test_scope()
    indexes = index(1,2,1,2)
    X = [1.0 2.0]
    XSS = [3.0 4.0]
    XPrime = [5.0 6.0]
    var_names = (:r, :w)

    @gen_equations
end

I get an error that UndefVarError: var_names not defined. My guess is that this is a scoping issue. So I defined the same macro but passed all objects explicitly to the macro

macro gen_equations(var_names, X, XSS, indexes)
    ...
end

But if I then try to define my function

function test_scope()
    indexes = index(1,2,1,2)
    X = [1.0 2.0]
    XSS = [3.0 4.0]
    XPrime = [5.0 6.0]
    var_names = (:r, :w)

    @gen_equations var_names, X, XSS, indexes
end

I get a MethodError: no method matching @gen_equations(::Expr).

Any hints would be greatly appreciated. I’m just starting getting my head around macros and I’m still mostly confused.

Setup is Julia 0.6.4 on MacOS.

Thank you very much,

Benjamin


#2

Can you give more context? Usually when you need a large number of variables, you just want to use an array, e.g.

r = zeros(100)

gives 100 variables called r[1] etc.


#3

Macros get the expressions of their arguments passed to them. For example given macro m(x) and the call @m 1+2, inside the macro x will equal the expression :(1+2).

Macros run right after code is parsed, so only the expressions are available. The assignment var_names = ... in your code will happen at run time, when test_scope() is called, which hasn’t happened yet. So in this case a solution would be to call the macro as @gen_equations r w, and write it as

macro gen_equations(var_names...)

The key is to distinguish which information is needed at macro-expansion time, and which information is needed at run time. Anything needed at macro-expansion time should be passed to the macro as an argument. Anything used at run time can be picked up from the surrounding scope.


#4

Thanks Jeff. That solved it and helped me understand the underlying mechanism much better.

Just a quick follow up. Is there a more elegant way to initialise the expression in the macro to which I append in the loop?

Thanks again, Benjamin


#5

Thanks David for your reply. I totally agree that I could also have one array that contains all the variable values. However the model is quite complex and it helps readability of the code to be able to write, e.g., r and rPrime instead of r[99] and r[100].


#6

The temp = 1 is not necessary; you can use just quote end or Expr(:block).


#7

I don’t know if you’ve already seen it but Parameters.jl @unpack macro (maybe also the @unpack!_TypeName macro - see the docs) might help. Similarly, StaticArrays.jl FieldVector type might also be useful.