Skip to content
AgileLogics
7 min read

Macros

Prerequisites: Metaprogramming: code as data

A macro is a function that runs when code is parsed: it receives expressions and returns a new expression that replaces the macro call. You've used many: @show, @time, @printf, @., @views, @test.

macro twice(ex)
    return quote
        $(esc(ex))
        $(esc(ex))
    end
end

@twice println("hello")

macro show_value(ex)
    text = string(ex)
    return :(println($text, " --> ", $(esc(ex))))
end

x = 5
@show_value x^2 + 1
hello
hello
x ^ 2 + 1 --> 26
  • macro name(args) ... end defines @name.
  • The arguments are expressions, not values: @show_value gets :(x^2 + 1) and can turn it into the string "x ^ 2 + 1" as well as evaluate it.
  • esc(ex) means "evaluate this in the caller's scope".

@macroexpand

See what a macro generates:

macro unless(cond, body)
    return :(if !$(esc(cond)); $(esc(body)); end)
end

ex = @macroexpand @unless 1 > 2 println("1 is not greater than 2")
println(Base.remove_linenums!(ex))
@unless 1 > 2 println("1 is not greater than 2")
if !(1 > 2)
    println("1 is not greater than 2")
end
1 is not greater than 2

Hygiene

Variables created inside a macro get unique names, so they can't clash with the caller's variables:

macro timed_twice(ex)
    quote
        t = time_ns()
        $(esc(ex))
        $(esc(ex))
        elapsed = time_ns() - t
        elapsed >= 0
    end
end

t = "my variable"
ok = @timed_twice print("hi ")
println(t, " ", ok)
hi hi my variable true

The macro's t was renamed behind the scenes, so the caller's t was untouched. Only esc-aped code refers to the caller's names.

A practical macro: @retry

macro retry(n, ex)
    quote
        local result
        for attempt in 1:$(esc(n))
            try
                result = $(esc(ex))
                break
            catch err
                attempt == $(esc(n)) && rethrow()
                println("attempt $attempt failed: ", sprint(showerror, err))
            end
        end
        result
    end
end

calls = Ref(0)
function flaky()
    calls[] += 1
    calls[] < 3 && error("service unavailable")
    return "ok after $(calls[]) calls"
end

println(@retry 5 flaky())
attempt 1 failed: service unavailable
attempt 2 failed: service unavailable
ok after 3 calls

A function couldn't do this: it would receive the result of flaky(), not the code to run again.

When to write a macro

Write a macro only when a function can't do the job — when you need to see the code itself (its text or structure), control whether or how often it runs, or create a small domain-specific language (like JuMP's @constraint or DataFrames' @df). Macros are harder to read and debug than functions.