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