llms.txt
Skip to main content

Macro Functions

Throughout this chapter, we have called quite a few functions whose names end with an exclamation mark: the assert! and assert_eq! macros in tests, and the vector macros such as map! and fold!. All of them are macro functions, and now that we know functions and generics, we have everything needed to understand how they work - and how to define our own.

What is a Macro Function?

A macro function looks and feels like a regular function, but it does not exist at runtime. Instead, the compiler expands the macro: at every call site, the body of the macro is substituted inline, with the arguments plugged into it, and only then is the resulting code type checked and compiled. A macro call is easy to recognize - the macro name is always followed by the ! mark.

This compile-time expansion gives macros two abilities that regular functions do not have:

  • They can take lambdas - inline blocks of code - as arguments. Move has no function values at runtime, but because a macro is expanded during compilation, the lambda simply becomes part of the generated code.
  • Their bodies are type checked after expansion, per call site, which permits operations that regular generics cannot express - as we are about to see.

Defining a Macro

A macro is defined with the macro fun keywords. The parameters - including type parameters - are prefixed with the $ sign, marking them as compile-time substitutions rather than runtime values:

/// Returns the larger of the two values.
public macro fun max<$T>($a: $T, $b: $T): $T {
let a = $a;
let b = $b;
if (a > b) a else b
}

The max macro returns the larger of its two arguments. Note something remarkable about the body: it compares two values of the generic type $T with the > operator. A regular generic function could not do this - there is no ability constraint for "comparable", so fun max<T>(a: T, b: T) would not compile. The macro sidesteps the problem entirely: by the time the body is type checked, $T is already replaced with a concrete type at each call site:

assert_eq!(max!(1, 2), 2);
assert_eq!(max!(10u8, 5), 10);
assert_eq!(max!(100u128, 200), 200);

Also note the let a = $a; binding at the top of the body. A macro argument is substituted as an expression, not as a computed value: every occurrence of $a in the body would evaluate the argument expression again. Binding the argument to a local variable once is a good habit that avoids surprising double evaluation.

Lambda Arguments

The real power of macros comes from lambda parameters. A lambda type is written as |argument_types| (or |argument_types| -> return_type when it returns a value), and the caller passes the lambda inline, using the |arguments| expression syntax:

/// Calls the `$f` lambda `$n` times, passing in the iteration number.
public macro fun repeat($n: u64, $f: |u64|) {
let n = $n;
let mut i = 0;
while (i < n) {
$f(i);
i = i + 1;
}
}
let mut sum = 0;
repeat!(4, |i| sum = sum + i);
assert_eq!(sum, 6); // 0 + 1 + 2 + 3

A lambda can read and even modify the variables of the enclosing scope - the repeat! call above updates the local variable sum on every iteration. This is exactly the mechanism behind the vector macros: v.do!(|el| ...) is a macro with a lambda parameter, expanded into a plain loop at compilation time.

Lazy Evaluation

Because arguments are substituted rather than computed up front, an argument expression may be evaluated once, many times - or not at all. The assert! macro is a good illustration: in assert!(condition, EMyError), the error code expression is only evaluated when the condition fails. This is a feature - the failure branch costs nothing on the happy path - but it is also the flip side of the double-evaluation caveat above: when writing your own macros, think about how many times each $ parameter is actually used.

Expansion at the call site has one more visible effect: an abort raised inside a macro body reports the line number of the macro call, not a line inside the macro definition. This is part of the clever error encoding, and it is why a failing assert! or assert_eq! points at the line in your code rather than somewhere in the standard library - a good reason to prefer a macro over a regular function when writing assertion helpers.

Macros in the Standard Library

The Standard Library makes heavy use of macros, and they are the idiomatic way to work with its core types. We have already seen the vector macros; Option and the integer types have their own sets:

// `Option` macros: `destroy_or!` unwraps the value with a default...
let opt = option::some(10);
assert_eq!(opt.destroy_or!(0), 10);

// ...and `map!` transforms the inner value, if it is present.
let doubled = option::some(5).map!(|x| x * 2);
assert_eq!(doubled, option::some(10));

// Integer macros iterate over numbers without a `while` loop.
let mut sum = 0u64;
10u64.do!(|i| sum = sum + i);
assert_eq!(sum, 45); // 0 + 1 + ... + 9

// And the `assert_eq!` macro, used all over this book, is
// defined in the `std::unit_test` module.

A quick overview of where to find them:

  • std::vector - do!, map!, filter!, fold!, count!, any!, all!, tabulate!, and more;
  • std::option - do!, map!, destroy_or!, extract_or!, is_some_and!;
  • integer modules, e.g. std::u64 - do!, range_do!, max_value!;
  • std::unit_test - assert_eq! and assert_ref_eq!, available in tests.

This section covers the day-to-day use of macros; the full feature set - including method syntax for macros, $ expressions in type positions, and hygiene rules - is described in the Move Reference.

Further Reading

llms.txt