Move 2024: Macro Functions Guide

Move 2024 beta edition now gives developers the ability to replace common loops with macro functions.

Move 2024: Macro Functions Guide

Move 2024 beta now includes macro functions! 

Macro functions in Move are similar to regular functions: you define the parameters, return type, and logic that determines how your macro functions operate, and then you can then call your macro functions from anywhere in your code. Unlike regular functions, however, the compiler expands those functions in place to execute the defined operations inline.

The main differences that separate macro functions from normal functions are: 

  • Macro function syntax extensions
  • Move compiler expansion and call semantics
  • Lambda parameters

Syntax extensions: Move syntax specifies how to define macro functions, their arguments, and their invocation. These rules not only help the compiler differentiate macros from normal functions, but also provide a visual differentiation between normal and macro functions during usage.

Compiler expansion and call semantics: Move eagerly evaluates normal function arguments. In other words, the runtime fully evaluates the arguments you pass to a normal function, whether necessary to the function logic or not. In contrast, macro functions directly substitute the expressions for their arguments during expansion, and the runtime won’t see the macro call at all! This means that arguments aren’t evaluated at all if the macro body code with the argument is never reached. For example, if the argument is substituted in an if-else expression and the branch with that argument is never taken, the argument will not be evaluated.

Lambda parameters: In addition to normal expressions, Macro functions allow you to supply parameterized code as higher-order function arguments. This is made possible with the introduction of the lambda type, allowing macro authors to create powerful and flexible macros. 

For more details on how to define macro functions, see the official reference in the Move book.

Anatomy of macro functions

The Move compiler expands macro functions during compilation, enabling expressiveness not available in normal functions. To indicate this special substitution behavior, macro functions have their type parameters and expression parameters prefixed with a $ character. Consider a Move function titled `my_assert` that behaves like the compiler primitive assert! (without clever errors). 

macro fun my_assert($cond: bool, $code: u64) {
    if (!$cond) abort $code.
}

The Move compiler substitutes the arguments to type parameters and expression parameters during expansion. This behavior creates the lazy evaluation process for arguments mentioned in the previous section. As the code shows, the `my_assert` macro checks the condition passed to it, aborting the program if the condition `$cond` resolves to false (or not `true`) with the `$code` passed as a second argument. 

To invoke the my_assert macro function, the programmer uses a ! character after the function name to visually indicate that the arguments are not eagerly evaluated:

my_assert!(vector[] == vector[], 0);
my_assert!(true, 1 / 0); // will not abort! '1 / 0' is not evaluated.

As you might notice, the $code value passed to the macro in the second invocation includes a division by zero operation. Although a bit contrived, its purpose is to show that the compiler will not evaluate the expression because control flow will never reach its execution due to the provided true condition.     

Macros with lambda arguments

The addition of macro functions includes the introduction of an entirely new feature: lambda. Using parameters with lambda types, you can pass code from the caller into the body of the macro. While the substitution is done at compile time, they are used similarly to anonymous functions, lambdas, or closures in other languages.

For example, consider a loop that executes a lambda on each number from 0 to a number n that the code provides.

public macro fun do($n: u64, $f: |u64| -> ()) {
    let mut i = 0;
    let stop = $n;
    while (i < stop) {
        $f(i);
        i = i + 1;
    }
}

The lambda parameter ($f) is the second argument to the do macro. The lambda’s type is defined as |u64| -> (), which takes a u64 as input and returns (). The return type of () is implicit, so the argument could also be written simply as $f: |u64|

Your program logic could then use this macro to sum the numbers from 0 to 10 using the lambda parameter:

let mut sum = 0;
do!(10, |i| sum = sum + i);

Here, the code snippet `|i| sum = sum + i` defines the lambda for the macro invocation. When it is called in the macro’s body, its arguments are evaluated and bound in the body of the lambda. Note that, unlike macro calls, lambdas will completely evaluate their arguments before executing the lambda body.

Macro functions are hygenic, so the i in the lambda is not the same as the i in the do. See the Move reference for more details.)

Macros in the Move standard library

With the addition of macros, the move-stdlib library introduces a number of macro functions to capture common programming patterns.

Integer macros

For u64 and other integer types, the move-stdlib library provides a set of macro functions that simplify common loops. This post examines the u64 type, but the same macro functions are available for u8, u16, u32, u128, and u256

You have already seen one example, as the do function code is a macro function in std::u64

The full list of currently available macro functions for integer types are:

  • public macro fun do($stop: u64, $f: |u64|) Loops applying $f to each number from 0 to $stop (exclusive).
  • public macro fun do_eq($stop: u64, $f: |u64|) Loops applying $f to each number from 0 to $stop (inclusive).
  • public macro fun range_do($start: u64, $stop: u64, $f: |u64|) Loops applying $f to each number from $start to $stop (exclusive).
  • public macro fun range_do_eq($start: u64, $stop: u64, $f: |u64|) Loops applying $f to each number from $start to $stop (inclusive).

For a simplified example of applying the defined macro functions, consider the following loop:

let mut i = 0;
while (i < 10) {
    foo(i);
    i = i + 1;
}

You could rewrite this loop by implementing the do macro function for std::u64 as:

10u64.do!(|i| foo(i));

Similarly, you could make the following loop more concise using the u64::range_do_eq macro function:

let mut i = 1;
// loop from 1 to 10^8 by 10s
while (i <= 100_000_000) {
    foo(i);
    i = i * 10;
}

The result of the rewrite:

1u64.range_do_eq!(8, |i| foo(10u64.pow(i)));
While this is potentially easier to read, it will take more gas to execute.

vector macros

In a similar spirit to integers' do macro, you can iterate over the elements of a vector using the do function.

vector[1, 2, 3, 4, 5].do!(|x| foo(x));

This is equivalent to:

let mut v = vector[1, 2, 3, 4, 5];
v.reverse();
while (!v.is_empty()) {
    foo(v.pop_back());
}

The expanded code shows that this process consumes the vector. Use the do_ref and do_mut macros to iterate by reference or by mutable reference, respectively.

fun check_coins(coins: &vector<Coin<SUI>>) {
    coins.do_ref!(|coin| assert!(coin.value() > 0));
    /* expands to
    let mut i = 0;
    let n = coins.len();
    while (i < n) {
        let coin = &coins[i];
        assert!(coin.value() > 0);
        i = i + 1;
    }
    */
}
fun take_10(coins: &mut vector<Coin<SUI>>) {
    coins.do_mut!(|coin| transfer::public_transfer(coin.take(10), @0x42));
    /* expands to
    let mut i = 0;
    let n = coins.len();
    while (i < n) {
        let coin = &mut coins[i];
        transfer::public_transfer(coin.take(10), @0x42);
        i = i + 1;
    }
    */
}

In addition to iteration, you can modify and create vectors with macros. vector::tabulate creates a vector of length n by applying a lambda to each index.

fun powers_of_2(n: u64): vector<u64> {
    vector::tabulate(n, |i| 2u64.pow(i))
    /* expands to
    let mut v = vector[];
    let mut i = 0;
    while (i < n) {
        v.push_back(2u64.pow(i));
        i = i + 1;
    };
    v
    */
}

vector::map creates a new vector from an existing vector by applying a lambda to each element. While vector::map operates on a vector by value, the map_ref and map_mut macros operate on a vector by reference and mutable reference, respectively.

fun into_balances(coins: vector<Coin<SUI>>): vector<Balance<SUI>> {
    coins.map!(|coin| coin.into_balance())
    /* expands to
    let mut v = vector[];
    coins.reverse();
    while (!coins.is_empty()) {
        let coin = coins.pop_back();
        v.push_back(coin.into_balance());
    };
    v
    */
}

Similar to map, vector::filter creates a new vector from an existing vector by keeping only elements that satisfy a predicate.

fun non_zero_numbers(numbers: vector<u64>): vector<u64> {
    numbers.filter!(|n| n > 0)
    /* expands to
    let mut v = vector[];
    numbers.reverse();
    while (!numbers.is_empty()) {
        let n = numbers.pop_back();
        if (n > 0) {
            v.push_back(n);
        }
    };
    v
    */
}
See Table A at the end of this article for a list of currently available macro functions for vectors.

option macros

While Option is not a collection with more than one element, the type has do (and do_ref and do_mut) macros to easily access its value if it is some.

fun maybe_transfer(coin: Option<Coin<SUI>>, recipient: address) {
    coin.do!(|c| transfer::public_transfer(c, recipient))
    /* expands to
    if (coin.is_some()) transfer::public_transfer(coin.destroy_some(), recipient)
    else coin.destroy_none()
    */
}

destroy performs the same action as do, but perhaps a more useful macro is destroy_or which provides a default expression, which is evaluated only if the Option is none. The following example chains map, which applies the lambda to the value of the Option if it is some, and destroy_or to concisely convert an Option<Coin<SUI>> to a Balance<SUI>.

fun to_balance_opt(coin: Option<Coin<SUI>>): Balance<SUI> {
    coin.map!(|c| c.into_balance()).destroy_or!(Balance::zero())
    /* expands to
    let opt = if (coin.is_some()) Option::some(coins.destroy_some().into_balance())
              else Option::none();
    if (opt.is_some()) opt.destroy_some()
    else Balance::zero()
    */
}
See Table B at the end of this article for a list of currently available macro functions for Option.

Wrap up

Macro functions can simplify your code by converting common patterns into reusable macros. The hope is that macro functions can replace many, if not all, of your common loops (loop and while). Keep an eye out for more macro functions in the move-stdlib library, and soon the sui framework.

For more details on how macro functions work, see the Move book.

Table A: Currently available macro functions for vectors

Prepend public macro fun to each line below:


tabulate<$T>($n: u64, $f: |u64| -> $T): vector<$T>

Create a vector of length n by calling the function f on each index.

destroy<$T>($v: vector<$T>, $f: |$T|)

Destroy the vector v by calling f on each element and then destroying the vector. Does not preserve the order of elements in the vector (starts from the end of the vector).

do<$T>($v: vector<$T>, $f: |$T|)

Destroy the vector v by calling f on each element and then destroying the vector. Preserves the order of elements in the vector.

do_ref<$T>($v: &vector<$T>, $f: |&$T|)

Perform an action f on each element of the vector v. The vector is not modified.

do_mut<$T>($v: &mut vector<$T>, $f: |&mut $T|)

Perform an action f on each element of the vector v. The function f/ takes a mutable reference to the element.

map<$T, $U>($v: vector<$T>, $f: |$T| -> $U): vector<$U>

Map the vector v to a new vector by applying the function f to each element. Preserves the order of elements in the vector, first is called first.

map_ref<$T, $U>($v: &vector<$T>, $f: |&$T| -> $U): vector<$U>

Map the vector v to a new vector by applying the function f to each element. Preserves the order of elements in the vector, first is called first.

filter<$T: drop>($v: vector<$T>, $f: |&$T| -> bool): vector<$T>

Filter the vector v by applying the function f to each element. Return a new vector containing only the elements for which f returns true.

partition<$T>($v: vector<$T>, $f: |&$T| -> bool): (vector<$T>, vector<$T>)

Split the vector v into two vectors by applying the function f to each element. Return a tuple containing two vectors: the first containing the elements for which f returns true, and the second containing the elements for which f returns false.

find_index<$T>($v: &vector<$T>, $f: |&$T| -> bool): Option<u64>

Finds the index of first element in the vector v that satisfies the predicate f. Returns some(index) if such an element is found, otherwise none().

count<$T>($v: &vector<$T>, $f: |&$T| -> bool): u64

Count how many elements in the vector v satisfy the predicate f.

fold<$T, $Acc>($v: vector<$T>, $init: $Acc, $f: |$Acc, $T| -> $Acc): $Acc

Reduce the vector v to a single value by applying the function f to each element. Similar to fold_left in Rust and reduce in Python and JavaScript.

any<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool

Whether any element in the vector v satisfies the predicate f. If the vector is empty, returns false.

all<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool

Whether all elements in the vector v satisfy the predicate f. If the vector is empty, returns true.

zip_do<$T1, $T2>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2|)

Destroys two vectors v1 and v2 by calling f to each pair of elements. Aborts if the vectors are not of the same length. The order of elements in the vectors is preserved.

zip_do_reverse<$T1, $T2>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2|)

Destroys two vectors v1 and v2 by calling f to each pair of elements. Aborts if the vectors are not of the same length. Starts from the end of the vectors.

zip_do_ref<$T1, $T2>($v1: &vector<$T1>, $v2: &vector<$T2>, $f: |&$T1, &$T2|)

Iterate through v1 and v2 and apply the function f to references of each pair of elements. The vectors are not modified. Aborts if the vectors are not of the same length. The order of elements in the vectors is preserved.

zip_do_mut<$T1, $T2>($v1: &mut vector<$T1>, $v2: &mut vector<$T2>, $f: |&mut $T1, &mut $T2|)

Iterate through v1 and v2 and apply the function f to mutable references of each pair of elements. The vectors may be modified. Aborts if the vectors are not of the same length. The order of elements in the vectors is preserved.

zip_map<$T1, $T2, $U>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2| -> $U): vector<$U>

Destroys two vectors v1 and v2 by applying the function f to each pair of elements. The returned values are collected into a new vector. Aborts if the vectors are not of the same length. The order of elements in the vectors is preserved.

zip_map_ref<$T1, $T2, $U>($v1: &vector<$T1>, $v2: &vector<$T2>, $f: |&$T1, &$T2| -> $U): vector<$U> 

Iterate through v1 and v2 and apply the function f to references of each pair of elements. The returned values are collected into a new vector. Aborts if the vectors are not of the same length. The order of elements in the vectors is preserved.

Table B: Currently available macro functions for Option

Prepend public macro fun to each line below:


destroy<$T>($o: Option<$T>, $f: |$T|) 

Destroy Option<T> and call the closure f on the value inside if it holds one.

do<$T>($o: Option<$T>, $f: |$T|) 

Destroy Option<T> and call the closure f on the value inside if it holds one.

do_ref<$T>($o: &Option<$T>, $f: |&$T|) 

Execute a closure on the value inside t if it holds one.

do_mut<$T>($o: &mut Option<$T>, $f: |&mut $T|)

Execute a closure on the mutable reference to the value inside t if it holds one.

or<$T>($o: Option<$T>, $default: Option<$T>): Option<$T> 

Select the first Some value from the two options, or None if both are None. Equivalent to Rust's a.or(b).

and<$T, $U>($o: Option<$T>, $f: |$T| -> Option<$U>): Option<$U> 

If the value is Some, call the closure f on it. Otherwise, return None. Equivalent to Rust's t.and_then(f).

and_ref<$T, $U>($o: &Option<$T>, $f: |&$T| -> Option<$U>): Option<$U>

If the value is Some, call the closure f on it. Otherwise, return None. Equivalent to Rust's t.and_then(f).

map<$T, $U>($o: Option<$T>, $f: |$T| -> $U): Option<$U> 

Map an Option<T> to Option<U> by applying a function to a contained value. Equivalent to Rust's t.map(f).

map_ref<$T, $U>($o: &Option<$T>, $f: |&$T| -> $U): Option<$U>

Map an Option<T> value to Option<U> by applying a function to a contained value by reference. Original Option<T> is preserved. Equivalent to Rust's t.map(f).

filter<$T: drop>($o: Option<$T>, $f: |&$T| -> bool): Option<$T>

Return None if the value is None, otherwise return Option<T> if the predicate f returns true.

is_some_and<$T>($o: &Option<$T>, $f: |&$T| -> bool): bool

Return false if the value is None, otherwise return the result of the predicate f.

destroy_or<$T>($o: Option<$T>, $default: $T): $T

Destroy Option<T> and return the value inside if it holds one, or default otherwise. Equivalent to Rust's t.unwrap_or(default).


Note: this function is a more efficient version of destroy_with_default, as it does not evaluate the default value unless necessary. The destroy_with_default function should be deprecated in favor of this function.