Ricerca…
Osservazioni
Una guida dettagliata dei macro può essere trovata in The Rust Programming Language (aka The Book) .
lezione
I macro ci permettono di astrarre schemi sintattici che vengono ripetuti molte volte. Per esempio:
/// Computes `a + b * c`. If any of the operation overflows, returns `None`.
fn checked_fma(a: u64, b: u64, c: u64) -> Option<u64> {
let product = match b.checked_mul(c) {
Some(p) => p,
None => return None,
};
let sum = match a.checked_add(product) {
Some(s) => s,
None => return None,
};
Some(sum)
}
Notiamo che le due dichiarazioni match
sono molto simili: entrambe hanno lo stesso schema
match expression {
Some(x) => x,
None => return None,
}
Immagina di rappresentare lo schema sopra come try_opt!(expression)
, quindi potremmo riscrivere la funzione in sole 3 righe:
fn checked_fma(a: u64, b: u64, c: u64) -> Option<u64> {
let product = try_opt!(b.checked_mul(c));
let sum = try_opt!(a.checked_add(product));
Some(sum)
}
try_opt!
impossibile scrivere una funzione perché una funzione non supporta il ritorno anticipato. Ma potremmo farlo con una macro - ogni volta che abbiamo questi schemi sintattici che non possono essere rappresentati usando una funzione, possiamo provare ad usare una macro.
Definiamo una macro usando le macro_rules!
sintassi:
macro_rules! try_opt {
// ^ note: no `!` after the macro name
($e:expr) => {
// ^~~~~~~ The macro accepts an "expression" argument, which we call `$e`.
// All macro parameters must be named like `$xxxxx`, to distinguish from
// normal tokens.
match $e {
// ^~ The input is used here.
Some(x) => x,
None => return None,
}
}
}
Questo è tutto! Abbiamo creato la nostra prima macro.
(Provalo in Rust Playground )
Crea una macro HashSet
// This example creates a macro `set!` that functions similarly to the built-in
// macro vec!
use std::collections::HashSet;
macro_rules! set {
( $( $x:expr ),* ) => { // Match zero or more comma delimited items
{
let mut temp_set = HashSet::new(); // Create a mutable HashSet
$(
temp_set.insert($x); // Insert each item matched into the HashSet
)*
temp_set // Return the populated HashSet
}
};
}
// Usage
let my_set = set![1, 2, 3, 4];
ricorsione
Una macro può chiamarsi, come una ricorsione di funzione:
macro_rules! sum {
($base:expr) => { $base };
($a:expr, $($rest:expr),+) => { $a + sum!($($rest),+) };
}
Andiamo però con l'espansione della sum!(1, 2, 3)
:
sum!(1, 2, 3)
// ^ ^~~~
// $a $rest
=> 1 + sum!(2, 3)
// ^ ^
// $a $rest
=> 1 + (2 + sum!(3))
// ^
// $base
=> 1 + (2 + (3))
Limite di ricorsione
Quando il compilatore espande le macro troppo profondamente, si arrenderà. Di default il compilatore fallisce dopo l'espansione delle macro fino a 64 livelli, quindi ad esempio la seguente espansione causerà un errore:
sum!(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62)
// error: recursion limit reached while expanding the macro `sum`
// --> <anon>:3:46
// 3 |> ($a:expr, $($rest:expr),+) => { $a + sum!($($rest),+) };
// |> ^^^^^^^^^^^^^^^^
Quando viene raggiunto un limite di ricorsione, è necessario considerare il refactoring della macro, ad es
- Forse la ricorsione potrebbe essere sostituita dalla ripetizione?
- Forse il formato di input potrebbe essere cambiato in qualcosa di meno sofisticato, quindi non abbiamo bisogno di ricorsione per abbinarlo?
Se c'è un motivo legittimo per cui 64 livelli non sono sufficienti, puoi sempre aumentare il limite della cassa invocando la macro con l'attributo:
#![recursion_limit="128"]
// ^~~ set the recursion limit to 128 levels deep.
Più modelli
Una macro può produrre diversi output rispetto a diversi pattern di input:
/// The `sum` macro may be invoked in two ways:
///
/// sum!(iterator)
/// sum!(1234, iterator)
///
macro_rules! sum {
($iter:expr) => { // This branch handles the `sum!(iterator)` case
$iter.fold(0, |a, b| a + *b)
};
// ^ use `;` to separate each branch
($start:expr, $iter:expr) => { // This branch handles the `sum!(1234, iter)` case
$iter.fold($start, |a, b| a + *b)
};
}
fn main() {
assert_eq!(10, sum!([1, 2, 3, 4].iter()));
assert_eq!(23, sum!(6, [2, 5, 9, 1].iter()));
}
Specificatori di frammenti - Tipi di modelli
In $e:expr
, l' expr
è chiamato lo specificatore di frammento . Indica al parser che tipo di token il parametro $e
si aspetta. Rust fornisce una varietà di specificatori di frammenti, consentendo all'input di essere molto flessibile.
specifier | Descrizione | Esempi |
---|---|---|
ident | Identifier | x , foo |
path | Nome qualificato | std::collection::HashSet , Vec::new |
ty | genere | i32 , &T , Vec<(char, String)> |
expr | Espressione | 2+2 , f(42) , if true { 1 } else { 2 } |
pat | Modello | _ , c @ 'a' ... 'z' , (true, &x) , Badger { age, .. } |
stmt | dichiarazione | let x = 3 , return 42 |
block | Blocco delimitato da parentesi | { foo(); bar(); } , { x(); y(); z() } |
item | Articolo | fn foo() {} , struct Bar; , use std::io; |
meta | All'interno dell'attributo | cfg!(windows) , doc="comment" |
tt | Token tree | + , foo , 5 , [?!(???)] |
Si noti che un commento del doc /// comment
è trattato come #[doc="comment"]
in una macro.
macro_rules! declare_const_option_type {
(
$(#[$attr:meta])*
const $name:ident: $ty:ty as optional;
) => {
$(#[$attr])*
const $name: Option<$ty> = None;
}
}
declare_const_option_type! {
/// some doc comment
const OPT_INT: i32 as optional;
}
// The above will be expanded to:
#[doc="some doc comment"]
const OPT_INT: Option<i32> = None;
Segui set
Alcuni specificatori di frammenti richiedono che il token che segue sia uno di un set limitato, chiamato "follow set". Ciò consente una certa flessibilità per la sintassi di Rust per evolvere senza rompere le macro esistenti.
specifier | Segui set |
---|---|
expr , stmt | => , ; |
ty , path | => , = | ; : > [ { as where |
pat | => , = | if in |
ident , block , item , meta , tt | qualsiasi token |
macro_rules! invalid_macro {
($e:expr + $f:expr) => { $e + $f };
// ^
// `+` is not in the follow set of `expr`,
// and thus the compiler will not accept this macro definition.
($($e:expr)/+) => { $($e)/+ };
// ^
// The separator `/` is not in the follow set of `expr`
// and thus the compiler will not accept this macro definition.
}
Esportazione e importazione di macro
Esportare una macro per consentire ad altri moduli di utilizzarla:
#[macro_export]
// ^~~~~~~~~~~~~~~ Think of it as `pub` for macros.
macro_rules! my_macro { (..) => {..} }
Utilizzo di macro da altre casse o moduli:
#[macro_use] extern crate lazy_static;
// ^~~~~~~~~~~~ Must add this in order to use macros from other crates
#[macro_use] mod macros;
// ^~~~~~~~~~~~ The same for child modules.
Debug delle macro
(Tutti questi sono instabili, e quindi possono essere utilizzati solo da un compilatore notturno.)
log_syntax! ()
#![feature(log_syntax)]
macro_rules! logged_sum {
($base:expr) => {
{ log_syntax!(base = $base); $base }
};
($a:expr, $($rest:expr),+) => {
{ log_syntax!(a = $a, rest = $($rest),+); $a + logged_sum!($($rest),+) }
};
}
const V: u32 = logged_sum!(1, 2, 3);
Durante la compilazione, stamperà quanto segue sullo stdout:
a = 1, resto = 2, 3
a = 2, resto = 3
base = 3
--pretty espanso
Esegui il compilatore con:
rustc -Z unstable-options --pretty expanded filename.rs
Questo espande tutte le macro e quindi stampa il risultato espanso su stdout, ad es.
#![feature(prelude_import)]
#![no_std]
#![feature(log_syntax)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
const V: u32 = { false; 1 + { false; 2 + { false; 3 } } };
(Questo è simile al flag -E
nei compilatori C di gcc
e clang
.)