Re-posted from: https://bkamins.github.io/julialang/2022/01/14/macros.html
Introduction
Macros in Julia are super useful for defining domain specific languages
and this is taken advantage of by many packages like JuMP.jl, StatsModels.jl,
DataFramesMeta.jl, DataFrameMacros.jl, ….
This post was prompted by the discussion in this issue and is aimed to
highlight how macros should be properly invoked.
The examples were tested under Julia 1.7.0.
Preliminaries
A big advantage of macros is that they do not require parentheses when they are
called, e.g.:
julia> @time sin(1)
0.000001 seconds
0.8414709848078965
This avoids visual noise of the alternative syntax:
julia> @time(sin(1))
0.000001 seconds
0.8414709848078965
The rules of both types of invocation are explained in the Julia Manual:
Macros are invoked with the following general syntax:
@name expr1 expr2 ... @name(expr1, expr2, ...)
Note the distinguishing @ before the macro name and the lack of commas between
the argument expressions in the first form, and the lack of whitespace after
@name in the second form.
The explanation seems clear. However, sometimes it is tricky to tell what Julia
considers to be an expression. Let me give some examples.
Examples of non-obvious expression handling
I think the issue is best explained with this basic macro:
julia> macro m(args...)
show(args)
end
@m (macro with 1 method)
julia> @m 1 + 1
(:(1 + 1),)
julia> @m 1+1
(:(1 + 1),)
julia> @m 1 +1
(1, 1)
As you can see above when you write 1 + 1
and 1+1
then Julia treats it
as a single expression. However if you write 1 +1
then Julia considers it
to be two expressions.
The issue is especially tricky with tuples:
julia> @m(1, 1)
(1, 1)
julia> @m (1, 1)
(:((1, 1)),)
julia> @m 1, 1
(:((1, 1)),)
julia> @m 1 1
(1, 1)
In the first case a parenthesized style of macro call was used and we see that
the @m
macro received two arguments. In @m (1, 1)
since we put a space
after @m
the (1, 1)
is considered to be a tuple that was passed to it as a
single argument. Writing @m 1, 1
is interpreted in the same way, as when
defining a tuple you can omit passing parenthesis. Finally @m 1 1
is again
interpreted as passing two arguments to @m
because the first and the second
1
are separate expressions.
Conclusions
When writing macros always make sure to take care of understanding where the
boundaries of the expressions passed to it are or use the macro invocation style
that uses parentheses.
Let me give one final example. If you want to get the time in minutes that some
operation took do not write:
julia> @elapsed sleep(1) / 60
ERROR: MethodError: no method matching /(::Nothing, ::Int64)
as sleep(1) / 60
gets interpreted as a single expression.
Instead do
julia> (@elapsed sleep(1)) / 60
0.01670782215
or
julia> @elapsed(sleep(1)) / 60
0.016707725083333333