For my use case (generating a struct of four FunctionWrappers from a parsed string) I can use the following two syntaxes interchangeably, and the resulting FunctionWrappers themselves benchmark to the same thing using BenchmarkTools.jl. (Note that the below is a minimum example.)
function afunk(args)
...
@eval( return (some expression) )
end
or
macro amac(args)
...
return quote (some expression) end
end
I was looking at the documentation and noticed this:
" A macro maps a tuple of arguments to a returned expression, and the resulting expression is compiled directly rather than requiring a runtime eval call."
So I benchmarked the creation of the functions using both approaches and the macro version takes a near-zero time relative to the function approach which makes sense given the above as function creation would be handled at compile time by literally inserting the code in-place.
From this I would infer that (probably) all of the time the macro is preferable. But particularly so if the function is generated many times where generation speed may be important.
Do others agree? I am still getting to grips with metaprogramming so I am curious to know othersâ opinion on the above two methods. Perhaps there are other trade-offs that I am not aware of?
So In your case, a macro is better. A thing to watch when using macros is that, if anything changes that the generated code depends on (trickier to track than without meta-programming), it has to be regenerated. If macro expansion (code generation) is really slow, you may try refactoring/optimizing it.
Macros are just a convenience to allow you to write some terse syntax that is then mapped to some other syntax that would be painful to write by hand. They have absolutely no extra âpowersâ and are thus never strictly needed.
Using eval is fine if you use it for what it is meant, which is dynamically evaluating code. This is most often used in global scope to eg define a bunch of functions in a loop that only differ slightly from each other.
This form of function calling eval at the end is almost certainly not doing what you want it to, especially if you have a return statement inside do the eval call. Eval is always global in Julia so you are calling return in global scope, which doesnât make sense.
I certainly agree I donât understand why it works (I canât get my head around return in the global scope), and there may be unintended consequences that cause bugs in the future versions of our software, but thus far @eval( return ⌠) does seem to be doing what we need.
Indeed, my concern about potential unintended consequences is my main reason for asking this Q.
At the moment we have chosen that over the macro approach because we are writing software that should be very approachable for non-Julia programming biologists and didnât want to introduce the macro syntax.
The use case is that we have a number of models users might want to fit to their data, but we want the flexibility to freeze particular model parameters and fit on only the âunfrozenâ params. So to do this we generate the model functions with the frozen parameters âhardwiredâ in via our function that does @eval. Itâs working well so far.
A slightly tangential but demonstrative MWE of our approach is the code below which generates a function that N (user defined) exponential terms summed up plus a constant.
Any feedback welcome.
function expgen(terms)
unpacker = ""
for i in 0:(2*terms)
unpacker = string(unpacker, "a", i, ", ")
end
unpacker = string(unpacker, "= params")
unpacker_expression = Meta.parse(unpacker)
expsum = "a0"
for i in 1:2:(2*terms)
expsum = string(expsum, " + a", i, "*exp(-ti/a", i + 1, ")")
end
expsum_expression = Meta.parse(expsum)
@eval return((t, params) -> ($unpacker_expression; [$expsum_expression for ti in t]))
end
macro expgen(terms)
unpacker = ""
for i in 0:(2*terms)
unpacker = string(unpacker, "a", i, ", ")
end
unpacker = string(unpacker, "= params")
unpacker_expression = Meta.parse(unpacker)
expsum = "a0"
for i in 1:2:(2*terms)
expsum = string(expsum, " + a", i, "*exp(-ti/a", i + 1, ")")
end
expsum_expression = Meta.parse(expsum)
return quote (t, params) -> ($unpacker_expression; [$expsum_expression for ti in t]) end
end
fgen = expgen(10)
mgen = @expgen 10
t = collect(0.0:0.1:10.0)
params = [1.0 for i in 1:21]
Where fgen(t, params) == mgen(t, params) evaluates to true.
Edit: I just noticed that removing the return still works @eval ((t, params) -> ($unpacker_expression; [$expsum_expression for ti in t])) as long it is the last line of the function. Also having return at the beginning of the line is fine (before the @eval).
I have never, in all of my years with Julia, encountered a situation in which it makes sense to represent Julia code as strings, and I donât think this is such a situation either. Regardless of whether eval() is your tool of choice, building up Julia code through string manipulation and then trying to parse it is going to be brittle and unnecessarily difficult. Julia has such a nice way of representing Julia code as data structures that there should be no need to go through string concatenation.
Trying to generate code through string manipulation makes it trivially easy to accidentally introduce syntax errors or produce code that doesnât parse at all (or parses into something wildly different from what you expected), e.g. when you misplace a single ) or ].
Instead, any Julia expressions you need to construct can be built up by actually generating Expr objects. Even easier, you can use :() to quote code and splice in the values you care about. So rather than trying to build the string `âexp(-ti / a1)â, you can do:
The huge benefit is that the resulting expr is guaranteed to be parsed into an expression (by construction it cannot have mis-matched parentheses or anything like that), and you can use it to build up more complex expressions: