Move 2024: Macro Functions Guide
Move 2024 beta edition now gives developers the ability to replace common loops with macro functions.
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 thedo
function code is a macro function instd::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 from0
to$stop
(exclusive).public macro fun do_eq($stop: u64, $f: |u64|)
Loops applying$f
to each number from0
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.