Macro Functions
Macro functions are a way of defining functions that are expanded during compilation at each call site. The arguments of the macro are not evaluated eagerly like a normal function, and instead are substituted by expression. In addition, the caller can supply code to the macro via lambdas.
These expression substitution mechanics make macro
functions similar
to macros found in other programming languages;
however, they are more constrained in Move than you might expect from other languages. The
parameters and return values of macro
functions are still typed--though this can be partially
relaxed with the _
type. The upside of this restriction however, is that
macro
functions can be used anywhere a normal function can be used, which is notably helpful with
method syntax.
A more extensive syntactic macro system may come in the future.
Syntax
macro
functions have a similar syntax to normal functions. However, all type parameter names and
all parameter names must start with a $
. Note that _
can still be used by itself, but not as a
prefix, and $_
must be used instead.
<visibility>? macro fun <identifier><[$type_parameters: constraint],*>([$identifier: type],*): <return_type> <function_body>
For example, the following macro
function takes a vector and a lambda, and applies the lambda to
each element of the vector to construct a new vector.
macro fun map<$T, $U>($v: vector<$T>, $f: |$T| -> $U): vector<$U> {
let mut v = $v;
v.reverse();
let mut i = 0;
let mut result = vector[];
while (!v.is_empty()) {
result.push_back($f(v.pop_back()));
i = i + 1;
};
result
}
The $
is there to indicate that the parameters (both type and value parameters) do not behave like
their normal, non-macro counterparts. For type parameters, they can be instantiated with any type
(even a reference type &
or &mut
), and they will satisfy any constraint. Similarly for
parameters, they will not be evaluated eagerly, and instead the argument expression will be
substituted at each usage.
Lambdas
Lambdas are a new type of expression that can only be used with macro
s. These are used to 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.
As seen in the example above ($f: |$T| -> $U
), lambda types are defined with the syntax
|<type>,*| (-> <type>)?
A few examples
|u64, u64| -> u128 // a lambda that takes two u64s and returns a u128
|&mut vector<u8>| -> &mut u8 // a lambda that takes a &mut vector<u8> and returns a &mut u8
If the return type is not annotated, it is unit ()
by default.
// the following are equivalent
|&mut vector<u8>, u64|
|&mut vector<u8>, u64| -> ()
Lambda expressions are then defined at the call site of the macro
with the syntax
|(<identifier> (: <type>)?),*| <expression>
|(<identifier> (: <type>)?),*| -> <type> { <expression> }
Note that if the return type is annotated, the body of the lambda must be enclosed in {}
.
Using the map
macro defined above
let v = vector[1, 2, 3];
let doubled: vector<u64> = map!(v, |x| 2 * x);
let bytes: vector<vector<u8>> = map!(v, |x| std::bcs::to_bytes(&x));
And with type annotations
let doubled: vector<u64> = map!(v, |x: u64| 2 * x); // return type annotation optional
let bytes: vector<vector<u8>> = map!(v, |x: u64| -> vector<u8> { std::bcs::to_bytes(&x) });
Capturing
Lambda expressions can also refer to variables in the scope where the lambda is defined. This is sometimes called "capturing".
let res = foo();
let incremented = map!(vector[1, 2, 3], |x| x + res);
Any variable can be captured, including mutable and immutable references.
See the Examples section for more complicated usages.
Limitations
Currently, lambdas can only be used directly in the call of a macro
function. They cannot be bound
to a variable. For example, the following is code will produce an error:
let f = |x| 2 * x;
// ^^^^^^^^^ Error! Lambdas must be used directly in 'macro' calls
let doubled: vector<u64> = map!(vector[1, 2, 3], f);
Typing
Like normal functions, macro
functions are typed--the types of the parameters and return value
must be annotated. However, the body of the function is not type checked until the macro is
expanded. This means that not all usages of a given macro may be valid. For example
macro fun add_one<$T>($x: $T): $T {
$x + 1
}
The above macro will not type check if $T
is not a primitive integer type.
This can be particularly useful in conjunction with method syntax, where the function is not resolved until after the macro is expanded.
macro fun call_foo<$T, $U>($x: $T): &$U {
$x.foo()
}
This macro will only expand successfully if $T
has a method foo
that returns a reference &$U
.
As described in the hygiene section, foo
will be resolved based on the scope where
call_foo
was defined--not where it was expanded.
Type Parameters
Type parameters can be instantiated with any type, including reference types &
and &mut
. They
can also be instantiated with tuple types, though the utility of this
is limited currently since tuples cannot be bound to a variable.
This relaxation forces the constraints of a type parameter to be satisfied at the call site in a way that does not normally occur. It is generally recommended however to add all necessary constraints to a type parameter. For example
public struct NoAbilities()
public struct CopyBox<T: copy> has copy, drop { value: T }
macro fun make_box<$T>($x: $T): CopyBox<$T> {
CopyBox { value: $x }
}
This macro will expand only if $T
is instantiated with a type with the copy
ability.
make_box!(1); // Valid!
make_box!(NoAbilities()); // Error! 'NoAbilities' does not have the copy ability
The suggested declaration of make_box
would be to add the copy
constraint to the type parameter.
This then communicates to the caller that the type must have the copy
ability.
macro fun make_box<$T: copy>($x: $T): CopyBox<$T> {
CopyBox { value: $x }
}
One might reasonably ask then, why have this relaxation if the recommendation is not to use it? The
constraints on type parameters simply cannot be enforced in all cases because the bodies are not
checked until expansion. In the following example, the copy
constraint on $T
is not necessary in
the signature, but is necessary in the body.
macro fun read_ref<$T>($r: &$T): $T {
*$r
}
If however, you want to have an extremely relaxed type signature, it is instead recommended to use
the _
type.
_
Type
Normally, the _
placeholder type is used in expressions to allow for
partial annotations of type arguments. However, with macro
functions, the _
type can be used in
place of type parameters to relax the signature for any type. This should increase the ergonomics of
declaring "generic" macro
functions.
For example, we could take any combination of integers and add them together.
macro fun add($x: _, $y: _, $z: _): u256 {
($x as u256) + ($y as u256) + ($z as u256)
}
Additionally, the _
type can be instantiated multiple times with different types. For example
public struct Box<T> has copy, drop, store { value: T }
macro fun create_two($f: |_| -> Box<_>): (Box<u8>, Box<u16>) {
($f(0u8), $f(0u16))
}
If we declared the function with type parameters instead, the types would have to unify to a common type, which is not possible in this case.
macro fun create_two<$T>($f: |$T| -> Box<$T>): (Box<u8>, Box<u16>) {
($f(0u8), $f(0u16))
// ^^^^ Error! expected `u8` but found `u16`
}
...
let (a, b) = create_two!(|value| Box { value });
In this case, $T
must be instantiated with a single type, but inference finds that $T
must be
bound to both u8
and u16
.
There is a tradeoff however, as the _
type conveys less meaning and intention for the caller.
Consider map
macro from above re-declared with _
instead of $T
and $U
.
macro fun map($v: vector<_>, $f: |_| -> _): vector<_> {
There is no longer any indication of behavior of $f
at the type level. The caller must gain
understanding from comments or the body of the macro.
Expansion and Substitution
The body of the macro
is substituted into the call site at compile time. Each parameter is
replaced by the expression, not the value, of its argument. For lambdas, additional local
variables can have values bound within the context of the macro
body.
Taking a very simple example
macro fun apply($f: |u64| -> u64, $x: u64): u64 {
$f($x)
}
With the call site
let incremented = apply!(|x| x + 1, 5);
This will roughly be expanded to
let incremented = {
let x = { 5 };
{ x + 1 }
};
Again, the value of x
is not substituted, but the expression 5
is. This might mean that an
argument is evaluated multiple times, or not at all, depending on the body of the macro
.
macro fun dup($f: |u64, u64| -> u64, $x: u64): u64 {
$f($x, $x)
}
let sum = dup!(|x, y| x + y, foo());
is expanded to
let sum = {
let x = { foo() };
let y = { foo() };
{ x + y }
};
Note that foo()
will be called twice. Which would not happen if dup
were a normal function.
It is often recommended to create predictable evaluation behavior by binding arguments to local variables.
macro fun dup($f: |u64, u64| -> u64, $x: u64): u64 {
let a = $x;
$f(a, a)
}
Now that same call site will expand to
let sum = {
let a = { foo() };
{
let x = { a };
let y = { a };
{ x + y }
}
};
Hygiene
In the example above, the dup
macro had a local variable a
that was used to bind the argument
$x
. You might ask, what would happen if the variable was instead named x
? Would that conflict
with the x
in the lambda?
The short answer is, no. macro
functions are
hygienic, meaning that the expansion of macro
s and
lambdas will not accidentally capture variables from another scope.
The compiler does this by associating a unique number with each scope. When the macro
is expanded,
the macro body gets its own scope. Additionally, the arguments are re-scoped on each usage.
Modifying the dup
macro to use x
instead of a
macro fun dup($f: |u64, u64| -> u64, $x: u64): u64 {
let a = $x;
$f(a, a)
}
The expansion of the call site
// let sum = dup!(|x, y| x + y, foo());
let sum = {
let x#1 = { foo() };
{
let x#2 = { x#1 };
let y#2 = { x#1 };
{ x#2 + y#2 }
}
};
This is an approximation of the compiler's internal representation, some details are omitted for the simplicity of this example.
And each usage of an argument is re-scoped so that the different usages do not conflict.
macro fun apply_twice($f: |u64| -> u64, $x: u64): u64 {
$f($x) + $f($x)
}
let result = apply_twice!(|x| x + 1, { let x = 5; x });
Expands to
let result = {
{
let x#1 = { let x#2 = { 5 }; x#2 };
{ x#1 + x#1 }
}
+
{
let x#3 = { let x#4 = { 5 }; x#4 };
{ x#3 + x#3 }
}
};
Similar to variable hygiene, method resolution is also scoped to the macro definition. For example
public struct S { f: u64, g: u64 }
fun f(s: &S): u64 {
s.f
}
fun g(s: &S): u64 {
s.g
}
use fun f as foo;
macro fun call_foo($s: &S): u64 {
let s = $s;
s.foo()
}
The method call foo
will in this case always resolve to the function f
, even if call_foo
is
used in a scope where foo
is bound to a different function, such as g
.
fun example(s: &S): u64 {
use fun g as foo;
call_foo!(s) // expands to 'f(s)', not 'g(s)'
}
Due to this though, unused use fun
declarations might not get warnings in modules with macro
functions.
Control Flow
Similar to variable hygiene, control flow constructs are also always scoped to where they are defined, not to where they are expanded.
macro fun maybe_div($x: u64, $y: u64): u64 {
let x = $x;
let y = $y;
if (y == 0) return 0;
x / y
}
At the call site, return
will always return from the macro
body, not from the caller.
let result: vector<u64> = vector[maybe_div!(10, 0)];
Will expand to
let result: vector<u64> = vector['a: {
let x = { 10 };
let y = { 0 };
if (y == 0) return 'a 0;
x / y
}];
Where return 'a 0
will return to the block 'a: { ... }
and not to the caller's body. See the
section on labeled control flow for more details.
Similarly, return
in a lambda will return from the lambda, not from the macro
body and not from
the outer function.
macro fun apply($f: |u64| -> u64, $x: u64): u64 {
$f($x)
}
and
let result = apply!(|x| { if (x == 0) return 0; x + 1 }, 100);
will expand to
let result = {
let x = { 100 };
'a: {
if (x == 0) return 'a 0;
x + 1
}
};
In addition to returning from the lambda, a label can be used to return to the outer function. In
the vector::any
macro, a return
with a label is used to return from the entire macro
early
public macro fun any<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool {
let v = $v;
'any: {
v.do_ref!(|e| if ($f(e)) return 'any true);
false
}
}
The return 'any true
exits from the "loop" early when the condition is met. Otherwise, the macro
"returns" false
.
Method Syntax
When applicable, macro
functions can be called using method syntax. When
using method syntax, the evaluation of the arguments will change in that the first argument (the
"receiver" of the method) will be evaluated outside of the macro expansion. This example is
contrived, but will concisely demonstrate the behavior.
public struct S() has copy, drop;
public fun foo(): S { abort 0 }
public macro fun maybe_s($s: S, $cond: bool): S {
if ($cond) $s
else S()
}
Even though foo()
will abort, its return type can be used to start a method call.
$s
will not be evaluated if $cond
is false
, and under a normal non-method call, an argument of
foo()
would not be evaluated and would not abort. The following example demonstrates $s
not
being evaluated with an argument of foo()
.
maybe_s!(foo(), false) // does not abort
It becomes more clear as to why it does not abort when looking at the expanded form
if (false) foo()
else S()
However, when using method syntax, the first argument is evaluated before the macro is expanded. So
the same argument of foo()
for $s
will now be evaluated and will abort.
foo().maybe_s!(false) // aborts
We can see this more clearly when looking the expanded form
let tmp = foo(); // aborts
if (false) tmp
else S()
Conceptually, the receiver for a method call is bound to a temporary variable before the macro is expanded, which forces the evaluation and thus the abort.
Parameter Limitations
The parameters of a macro
function must always be used as expressions. They cannot be used in
situations where the argument might be re-interpreted. For example, the following is not allowed
macro fun no($x: _): _ {
$x.f
}
The reason is that if the argument $x
was not a reference, it would be borrowed first, which would
could re-interpret the argument. To get around this limitation, you should bind the argument to a
local variable.
macro fun yes($x: _): _ {
let x = $x;
x.f
}
Examples
Lazy arguments: assert_eq
macro fun assert_eq<$T>($left: $T, $right: $T, $code: u64) {
let left = $left;
let right = $right;
if (left != right) {
std::debug::print(&b"assertion failed.\n left: ");
std::debug::print(&left);
std::debug::print(&b"\n does not equal right: ");
std::debug::print(&right);
abort $code;
}
}
In this case the argument to $code
is not evaluated unless the assertion fails.
assert_eq!(vector[true, false], vector[true, false], 1 / 0); // division by zero is not evaluated
Any integer square root
This macro calculates the integer square root for any integer type, besides u256
.
$T
is the type of the input and $bitsize
is the number of bits in that type, for example u8
has 8 bits. $U
should be set to the next larger integer type, for example u16
for u8
.
In this macro
, the type of the integer literals are 1
and 0
are annotated, e.g. (1: $U)
allowing for the type of the literal to differ with each call. Similarly, as
can be used with the
type parameters $T
and $U
. This macro will then only successfully expand if $T
and $U
are
instantiated with the integer types.
macro fun num_sqrt<$T, $U>($x: $T, $bitsize: u8): $T {
let x = $x;
let mut bit = (1: $U) << $bitsize;
let mut res = (0: $U);
let mut x = x as $U;
while (bit != 0) {
if (x >= res + bit) {
x = x - (res + bit);
res = (res >> 1) + bit;
} else {
res = res >> 1;
};
bit = bit >> 2;
};
res as $T
}
Iterating over a vector
The two macro
s iterate over a vector, immutably and mutably respectively.
macro fun for_imm<$T>($v: &vector<$T>, $f: |&$T|) {
let v = $v;
let n = v.length();
let mut i = 0;
while (i < n) {
$f(&v[i]);
i = i + 1;
}
}
macro fun for_mut<$T>($v: &mut vector<$T>, $f: |&mut $T|) {
let v = $v;
let n = v.length();
let mut i = 0;
while (i < n) {
$f(&mut v[i]);
i = i + 1;
}
}
A few examples of usage
fun imm_examples(v: &vector<u64>) {
// print all elements
for_imm!(v, |x| std::debug::print(x));
// sum all elements
let mut sum = 0;
for_imm!(v, |x| sum = sum + x);
// find the max element
let mut max = 0;
for_imm!(v, |x| if (x > max) max = x);
}
fun mut_examples(v: &mut vector<u64>) {
// increment each element
for_mut!(v, |x| *x = *x + 1);
// set each element to the previous value, and the first to last value
let mut prev = v[v.length() - 1];
for_mut!(v, |x| {
let tmp = *x;
*x = prev;
prev = tmp;
});
// set the max element to 0
let mut max = &mut 0;
for_mut!(v, |x| if (*x > *max) max = x);
*max = 0;
}
Non-loop lambda usage
Lambdas do not need to be used in loops, and are often useful for conditionally applying code.
macro fun inspect<$T>($opt: &Option<$T>, $f: |&$T|) {
let opt = $opt;
if (opt.is_some()) $f(opt.borrow())
}
macro fun is_some_and<$T>($opt: &Option<$T>, $f: |&$T| -> bool): bool {
let opt = $opt;
if (opt.is_some()) $f(opt.borrow())
else false
}
macro fun map<$T, $U>($opt: Option<$T>, $f: |$T| -> $U): Option<$U> {
let opt = $opt;
if (opt.is_some()) {
option::some($f(opt.destroy_some()))
} else {
opt.destroy_none();
option::none()
}
}
And some examples of usage
fun examples(opt: Option<u64>) {
// print the value if it exists
inspect!(&opt, |x| std::debug::print(x));
// check if the value is 0
let is_zero = is_some_and!(&opt, |x| *x == 0);
// upcast the u64 to a u256
let str_opt = map!(opt, |x| x as u256);
}