Generate Functions in Julia

Viewed 97

I have two functions

"This is my docstring for `add_1(x)`"
add_1(x) = x + 1

"This is my docstring for `add_2(x)`"
add_2(x) = x + 2

The code of the two functions is strikingly similar, so that in Julia there must be a clever way of automating their creation.

Side question: If these functions were generated programmatically, how would errors work out? Usually the stack trace shows a line number in the originating file, but here several functions are generated from the same line. Would it always show the same line number as well?

2 Answers

One solution would be this

["""
"This is my docstring for \`add_$i(x)\`"
add_$i(x) = x + $i
""" for i ∈ 1:3] .|> Meta.parse .|> eval

Regarding the line numbers, however, I am stuck. This is what you get by deliberately causing an error:

julia> add_3("arstoen")
ERROR: MethodError: no method matching +(::String, ::Int64)
Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...) at operators.jl:560
  +(::T, ::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16,         UInt32, UInt64, UInt8} at int.jl:87
  +(::LinearAlgebra.UniformScaling, ::Number) at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.6/LinearAlgebra/src/uniformscaling.jl:145
  ...
Stacktrace:
 [1] add_3(x::String)
   @ Main ./none:2
 [2] top-level scope
   @ REPL[8]:1

Does anyone else know a way of making the stacktrace sensible?

You need to manually built abstract syntax tree (AST) of the code and inject appropriate LineNumberNodes.

Let's start with some AST. It is usually easiest to have Julia generate the initial AST at the beginning and than mutate it.

code = quote
  add_1(x) = x+1
end

Try running dump(code) to see the AST (I omit it here as the output is long).

Now we can mutate it to generate add_2. You can put the code below in a function and generate is many variants of add_n as you want but for readability I will focus just on add_2.

code.args[1] = LineNumberNode(2, Symbol("My line of add_2"))
code.args[2].args[1] = :(add_2(x))
code.args[2].args[2].args[1] =  LineNumberNode(2, Symbol("My function body of add two"))
code.args[2].args[2].args[2].args[3]=2

That's it! You have your custom function with custom line numbering. Let's test it!

julia> eval(code)
add_2 (generic function with 1 method)

julia> add_2(4)
6

julia> add_2("will errror")
ERROR: MethodError: no method matching +(::String, ::Int64)
  [...]
Stacktrace:
 [1] add_2(x::String)
   @ Main .\My function body of add two:2
 [2] top-level scope
   @ REPL[22]:1

You can see that you can inject any location information in your generated code as you want.

For further reading have a look at: https://docs.julialang.org/en/v1/devdocs/ast/

Related