Macro to automatically create large number of variables

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

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.

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.

2 Likes

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

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].

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

1 Like

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.