I was recently implementing a bunch of functions which all had multiple methods with identical structure. The methods differed only in their arguments and some preprocessing steps. EDIT (borrowed from a later post): The general form of these functions can be described like this:
- We have
n
functionsf_1
,f_2
, …,f_n
. - For each function, we have
m
methods. - Each method has the form
f_j(x_k,y_j) = (expr_k ; expr_j)
, wherex_k
are arguments that depend only on the method andy_j
are arguments that depend only on the function, andexpr_k
is a code block that depends only on the method andexpr_j
is a code block that depends only on the function.
I would like to be able to automatically generate all the methods for all the functions, while only specifying x_k
, y_j
, expr_k
, and expr_j
once. I have a solution for this problem, but I suspect there exist much better solutions.
Here’s a toy example to illustrate the problem, with n=2 and m=2:
struct Foo
data::Vector{Float64}
xMax::Real
n::Int
end
function linearFoo(xMax::Real,n::Int,a::Real,b::Real)
x = (1:n) .* xMax # Boilerplate
data = a .+ b .* x # This is the important line
return Foo(data, xMax, n)
end
function linearFoo(f::Foo,a::Real,b::Real)
xMax = f.xMax # Boilerplate
n = f.n # Boilerplate
x = (1:n) .* xMax # Boilerplate
data = a .+ b .* x # This is the important line
return Foo(data, xMax, n)
end
function quadraticFoo(xMax::Real,n::Int,a::Real,b::Real,c::Real)
x = (1:n) .* xMax
data = a .+ b .* x .+ c .* x.^2
return Foo(data, xMax, n)
end
function quadraticFoo(f::Foo,a::Real,b::Real,c::Real)
xMax = f.xMax
n = f.n
x = (1:n) .* xMax
data = a .+ b .* x .+ c .* x.^2
return Foo(data, xMax, n)
end
I’m a complete novice at metaprogramming, but this seemed like something I could automate with a macro. I managed to do so, but I’m not sure that my solution was remotely optimal. I’m interested to know if anyone else has dealt with similar issues and has an elegant solution. Here’s what I came up with:
# Template for the first method
methodPattern1 = quote
function makeFoo(xMax::Real,n::Int)
x = (1:n) .* xMax
end
end
# Template for the second method
methodPattern2 = quote
function makeFoo(f::Foo)
xMax = f.xMax
n = f.n
x = (1:n) .* xMax
end
end
# Helpers to extract template data
patternRef_args(x::Expr) = x.args[2].args[1].args # push! arguments here
patternRef_body(x::Expr) = x.args[2].args[2].args # push! function body here
# Helpers to extract new function data
funcRef_name(x::Expr) = x.args[1].args[1]
funcRef_args(x::Expr) = x.args[1].args[2:end]
funcRef_body(x::Expr) = x.args[2]
# Update the pattern with the new function data
function updateFuncExpr!(pattern,func)
patternRef_args(pattern)[1] = funcRef_name(func)
push!(patternRef_args(pattern),funcRef_args(func)...)
push!(patternRef_body(pattern),funcRef_body(func))
end
# Put it all together
macro addTemplateMethods(funcExpr)
patt1 = copy(methodPattern1)
updateFuncExpr!(patt1,funcExpr)
patt2 = copy(methodPattern2)
updateFuncExpr!(patt2,funcExpr)
return quote
$(esc(patt1))
$(esc(patt2))
end
end
Example usage:
@addTemplateMethods function cubicFoo(a::Real,b::Real,c::Real,d::Real)
data = a .+ b .* x .+ c .* x.^2 .+ d .* x.^3
return Foo(data, xMax, n)
end
cubicFoo(10,20,1,2,3,4)
The real thing was a bit more complicated, as I handled parametric types and keyword args, but it had the same flavor. It nicely simplifies the process of adding both new functions and new methods (e.g. if I wanted a method that took a Foo
argument, but allowed you to overwrite the xMax
parameter). However, the metaprogramming code that extracts parts of the Expr tree is very opaque and a bit fragile, and I wonder if there’s a better way to accomplish this.
Any suggestions for how to improve this?