Am I using macros properly to avoid code repetition?

I want to define a macro inside a module MyAddConstrs, hoping that I can avoid code repetition therein. The original code is

module MyAddConstrs
import JuMP

_gen2(model, GD, p, t, s) = JuMP.@expression(model,
    sum(p[t,s,z,g,:u]-p[t,s,z,g,:d] for z=GD.Zone for g=eachindex(GD.N[z]))
)

add(model, t, s, Other, Reserve, Wind, Demand, Load, p, p0, ϖ, ζ) = JuMP.@constraint(model,
    [t=t, s=s],
    _gen(model, Other, Reserve, Wind, Demand, t, s, p, p0, ϖ, ζ) ==
    -_Const(Wind, Load, Demand, t, s)
)
add(model, t, s, Other, Reserve, Wind, Demand, Load, p, p0, ϖ, ζ, δ) = JuMP.@constraint(model,
    [t=t, s=s],
    _gen2(model, Reserve, δ, t, s) +
    _gen(model, Other, Reserve, Wind, Demand, t, s, p, p0, ϖ, ζ) ==
    -_Const(Wind, Load, Demand, t, s)
)

end

Seemingly it is complicated. But actually it’s very simple—the second add method at the bottom only has one more term at the left-hand-side, compared to the first add method (and one more arg δ). The _gen2 is an ordinary function defined as shown.

Now I seek to replace the two JuMP.@constraint body with two macro calls, so that the new module reads

module MyAddConstrs
import JuMP

_gen2(model, GD, p, t, s) = ... # unaltered

macro _m(e) return esc(quote
    JuMP.@constraint(model, [t=t, s=s],
        $e + _gen(model, Other, Reserve, Wind, Demand, t, s, p, p0, ϖ, ζ) ==
        -_Const(Wind, Load, Demand, t, s)
    )
end) end

add(model, t, s, Other, Reserve, Wind, Demand, Load, p, p0, ϖ, ζ) = @_m(JuMP.AffExpr(0))
add(model, t, s, Other, Reserve, Wind, Demand, Load, p, p0, ϖ, ζ, δ) = @_m(_gen2(model, Reserve, δ, t, s))
 
end

I wonder if I’m attaining my aim properly. (The final numeric results seem to be identical though)

It looks likely to be fine, although I never like trying to reason about how nested macros interact.

If it was my code I would try to reduce the duplication with an optional function argument rather than a macro, something like

add(model, t, s, Other, Reserve, Wind, Demand, Load, p, p0, ϖ, ζ, δ = nothing) = JuMP.@constraint(model,
    [t=t, s=s],
    _gen(model, Other, Reserve, Wind, Demand, t, s, p, p0, ϖ, ζ, δ) ==
    -_Const(Wind, Load, Demand, t, s)
)

function _gen(model, Other, Reserve, Wind, Demand, t, s, p, p0, ϖ, ζ, δ)
    x = ...
    if !isnothing(δ)
        x += _gen2(model, Reserve, δ, t, s)
    end
    return x
end
2 Likes

Thanks for the advice.

The main concern about your solution is that you introduced a if-branch inside the body of JuMP.@constraint, which could to a certain extent be adverse to performance. The other minor concern is that _gen2(..., ..., ....) returns a JuMP expression, so the x += _gen2(...) is outside of the JuMP’s macro.

These are very JuMP-related performance concerns, which is unrelated to the general idea.

If my macro method works good, I’ll probably use that in practice. That looks nice—as long as I confine those code to be local, inside the module MyAddConstrs. And I only call MyAddConstrs.add from outside.

I won’t try to predict what the JuMP macros do, but in normal functions this kind of if-branch would be eliminated during compilation thanks to method specialization and constant propagation.

1 Like