Move 2024 beta ora include
macro
funzioni!
Le funzioni macro in Move sono simili alle funzioni normali: definisci i parametri, il tipo di ritorno e la logica che determina come operano le tue funzioni macro, e poi puoi chiamare le tue funzioni macro da qualsiasi punto del tuo codice. A differenza delle funzioni normali, tuttavia, il compilatore espande quelle funzioni in loco per eseguire le operazioni definite in linea.
Le principali differenze che separano le funzioni macro dalle funzioni normali sono:
Estensioni della sintassi delle funzioni macro
Spostare l'espansione del compilatore e chiamare la semantica
Parametri Lambda
Estensioni di sintassi: la sintassi Move specifica come definire le funzioni macro, i loro argomenti e la loro invocazione. Queste regole non solo aiutano il compilatore a differenziare le macro dalle funzioni normali, ma forniscono anche una differenziazione visiva tra funzioni normali e macro durante l'uso.
Espansione del compilatore e semantica delle chiamate: Move valuta con entusiasmo gli argomenti delle funzioni normali. In altre parole, il runtime valuta completamente gli argomenti che passi a una funzione normale, che siano necessari o meno alla logica della funzione. Al contrario, le funzioni macro sostituiscono direttamente le espressioni per i loro argomenti durante l'espansione e il runtime non vedrà affatto la chiamata macro! Ciò significa che gli argomenti non vengono affatto valutati se il codice del corpo della macro con l'argomento non viene mai raggiunto. Ad esempio, se l'argomento viene sostituito in un'espressione if-else e il ramo con quell'argomento non viene mai preso, l'argomento non verrà valutato.
Parametri Lambda: oltre alle espressioni normali, le funzioni Macro consentono di fornire codice parametrizzato come argomenti di funzione di ordine superiore. Ciò è reso possibile dall'introduzione del tipo lambda, che consente agli autori di macro di creare macro potenti e flessibili.
Per maggiori dettagli su come definire le funzioni macro, consultare il riferimento ufficiale nel libro Move.
Anatomia delle funzioni macro
Il compilatore Move espande le funzioni macro durante la compilazione, consentendo un'espressività non disponibile nelle funzioni normali. Per indicare questo speciale comportamento di sostituzione, le funzioni macro hanno i loro parametri di tipo e i parametri di espressione preceduti da un
$
carattere. Considera una funzione Move intitolata `my_assert` che si comporta come la primitiva del compilatore assert! (senza errori intelligenti).
macro fun my_assert($cond: bool, $code: u64) { if (!$cond) abort $code. }
Il compilatore Move sostituisce gli argomenti con i parametri di tipo e i parametri di espressione durante l'espansione. Questo comportamento crea il processo di valutazione pigro per gli argomenti menzionati nella sezione precedente. Come mostra il codice, la macro `my_assert` controlla la condizione che le è stata passata, interrompendo il programma se la condizione `$cond` si risolve in
falso
(o non `true`) con `$code` passato come secondo argomento.
Per invocare il
la mia_affermazione
funzione macro, il programmatore usa un carattere ! dopo il nome della funzione per indicare visivamente che gli argomenti non vengono valutati con urgenza:
my_assert!(vector[] == vector[], 0); my_assert!(true, 1 / 0); // non verrà annullato! '1 / 0' non viene valutato.
Come potresti notare, il
$codice
il valore passato alla macro nella seconda invocazione include un'operazione di divisione per zero. Sebbene un po' forzata, il suo scopo è mostrare che il compilatore non valuterà l'espressione perché il flusso di controllo non raggiungerà mai la sua esecuzione a causa della condizione true fornita.
Macro con argomenti lambda
L'aggiunta di funzioni macro include l'introduzione di una funzionalità completamente nuova: lambda. Utilizzando parametri con tipi lambda, è possibile passare codice dal chiamante al corpo della macro. Mentre la sostituzione viene eseguita in fase di compilazione, vengono utilizzate in modo simile alle funzioni anonime, lambda o closure in altri linguaggi.
Ad esempio, si consideri un ciclo che esegue una lambda su ogni numero da
0
ad un numero n fornito dal codice.
macro pubblica fun do($n: u64, $f: |u64| -> ()) { let mut i = 0; let stop = $n; while (i < stop) { $f(i); i = i + 1; } }
Il parametro lambda (
$f
) è il secondo argomento della macro do. Il tipo di lambda è definito come |u64| -> (), che accetta u64 come input e restituisce (). Il tipo di ritorno di () è implicito, quindi l'argomento potrebbe anche essere scritto semplicemente come $f: |u64|.
La logica del tuo programma potrebbe quindi utilizzare questa macro per sommare i numeri da
0
a 10 utilizzando il parametro lambda:
lascia mut somma = 0; fai!(10, |i| somma = somma + i);
Qui, il frammento di codice `|i| sum = sum + i` definisce la lambda per l'invocazione della macro. Quando viene chiamata nel corpo della macro, i suoi argomenti vengono valutati e vincolati nel corpo della lambda. Nota che, a differenza delle chiamate macro, le lambda valuteranno completamente i loro argomenti prima di eseguire il corpo della lambda.
Le funzioni macro sono igieniche, quindi la i in lambda non è la stessa della i in do. Vedere il riferimento Move per maggiori dettagli.)
Macro nella libreria standard Move
Con l'aggiunta di macro, il
sposta-stdlib
La libreria introduce una serie di funzioni macro per catturare modelli di programmazione comuni.
Macro intere
Per
u64
e altri tipi di interi, la libreria move-stdlib fornisce un set di funzioni macro che semplificano i loop comuni. Questo post esamina il tipo u64, ma le stesse funzioni macro sono disponibili per u8, u16, u32, u128 e u256.
Hai già visto un esempio, poiché il codice della funzione do è una funzione macro in std::u64.
L'elenco completo delle funzioni macro attualmente disponibili per i tipi interi è:
macro pubblica fun do($stop: u64, $f: |u64|)
Cicli che applicano $f a ciascun numero da 0 a $stop (escluso).
macro pubblica fun do_eq($stop: u64, $f: |u64|)
Cicli che applicano $f a ciascun numero da 0 a $stop (inclusi).
macro pubblica fun range_do($start: u64, $stop: u64, $f: |u64|)
Cicli che applicano $f a ciascun numero da $start a $stop (esclusi).
macro pubblica fun range_do_eq($start: u64, $stop: u64, $f: |u64|)
Cicli che applicano $f a ciascun numero da $start a $stop (inclusi).
Per un esempio semplificato di applicazione delle funzioni macro definite, si consideri il seguente ciclo:
lascia mut i = 0; while (i < 10) { foo(i); i = i + 1; }
Potresti riscrivere questo ciclo implementando il
Fare
funzione macro per std::u64 come:
10u64.do!(|i| foo(i));
Allo stesso modo, potresti rendere il seguente ciclo più conciso utilizzando
funzione macro u64::range_do_eq
:
lascia mut i = 1; // ciclo da 1 a 10^8 di 10 secondi while (i <= 100_000_000) { foo(i); i = i * 10; }
Il risultato della riscrittura:
1u64.range_do_eq!(8, |i| foo(10u64.pow(i)));
Sebbene questa sia potenzialmente più facile da leggere, la sua esecuzione richiederà più carburante.
vettore
macro
In uno spirito simile agli interi
Fare
macro, è possibile scorrere gli elementi di un vettore utilizzando la funzione do.
vettore[1, 2, 3, 4, 5].do!(|x| foo(x));
Ciò equivale a:
lascia mut v = vettore[1, 2, 3, 4, 5]; v.reverse(); while (!v.is_empty()) { foo(v.pop_back()); }
Il codice espanso mostra che questo processo consuma il vettore. Utilizzare il
fare_riferimento
e macro do_mut per iterare rispettivamente per riferimento o per riferimento modificabile.
divertimento check_coins(coins: &vector<Coin<SUI>>) { coins.do_ref!(|coin| assert!(coin.value() > 0)); /* si espande in 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)); /* si espande in let mut i = 0; let n = coins.len(); while (i < n) { let coin = &mut coins[i]; transfer::public_transfer(coin.take(10), @0x42); io = io + 1; } */ }
Oltre all'iterazione, è possibile modificare e creare vettori con le macro.
vettore::tabulare
crea un vettore di lunghezza n applicando una lambda a ciascun indice.
divertimento powers_of_2(n: u64): vector<u64> { vector::tabulate(n, |i| 2u64.pow(i)) /* si espande in let mut v = vector[]; let mut i = 0; while (i < n) { v.push_back(2u64.pow(i)); i = i + 1; }; v */ }
vettore::mappa
crea un nuovo vettore da un vettore esistente applicando una lambda a ogni elemento. Mentre vector::map opera su un vettore per valore, le macro map_ref e map_mut operano su un vettore per riferimento e riferimento mutabile, rispettivamente.
divertimento into_balances(monete: vettore<Coin<SUI>>): vettore<Balance<SUI>> { coins.map!(|coin| coin.into_balance()) /* si espande in let mut v = vettore[]; coins.reverse(); while (!coins.is_empty()) { let coin = coins.pop_back(); v.push_back(coin.into_balance()); }; v */ }
Simile alla mappa,
vettore::filtro
crea un nuovo vettore da un vettore esistente mantenendo solo gli elementi che soddisfano un predicato.
fun non_zero_numbers(numbers: vector<u64>): vector<u64> { numbers.filter!(|n| n > 0) /* si espande in let mut v = vector[]; numbers.reverse(); while (!numbers.is_empty()) { let n = numbers.pop_back(); if (n > 0) { v.push_back(n); } }; in */ }
Per un elenco delle funzioni macro attualmente disponibili per i vettori, vedere la Tabella A alla fine di questo articolo.
opzione
macro
Mentre
Opzione
non è una raccolta con più di un elemento, il tipo ha macro do (e do_ref e do_mut) per accedere facilmente al suo valore se è presente.
divertimento maybe_transfer(coin: Option<Coin<SUI>>, recipient: address) { coin.do!(|c| transfer::public_transfer(c, recipient)) /* si espande in if (coin.is_some()) transfer::public_transfer(coin.destroy_some(), recipient) else coin.destroy_none() */ }
distruggere
esegue la stessa azione di do, ma forse una macro più utile è destroy_or che fornisce un'espressione predefinita, che viene valutata solo se Option è none. Il seguente esempio concatena la mappa, che applica la lambda al valore di Option se è some, e destroy_or per convertire in modo conciso un Option<Coin<SUI>> in un Balance<SUI>.
divertimento to_balance_opt(coin: Option<Coin<SUI>>): Balance<SUI> { coin.map!(|c| c.into_balance()).destroy_or!(Balance::zero()) /* si espande in 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() */ }
Per un elenco delle funzioni macro attualmente disponibili per Option, vedere la Tabella B alla fine di questo articolo.
Incartare
Le funzioni macro possono semplificare il tuo codice convertendo pattern comuni in macro riutilizzabili. La speranza è che le funzioni macro possano sostituire molti, se non tutti, i tuoi loop comuni (loop e while). Tieni d'occhio altre funzioni macro nella libreria move-stdlib e presto nel framework sui.
Per maggiori dettagli sul funzionamento delle funzioni macro, consultare il libro Move.
Tabella A: Funzioni macro attualmente disponibili per i vettori
Aggiungere in anteposizione
divertimento macro pubblico
per ogni riga sottostante:
tabula<$T>($n: u64, $f: |u64| -> $T): vettore<$T>
Crea un vettore di lunghezza
N
chiamando la funzione f su ciascun indice.
distruggere<$T>($v: vettore<$T>, $f: |$T|)
Distruggi il vettore
v
chiamando f su ogni elemento e poi distruggendo il vettore. Non preserva l'ordine degli elementi nel vettore (inizia dalla fine del vettore).
do<$T>($v: vettore<$T>, $f: |$T|)
Distruggi il vettore
v
chiamando f su ogni elemento e quindi distruggendo il vettore. Preserva l'ordine degli elementi nel vettore.
do_ref<$T>($v: &vettore<$T>, $f: |&$T|)
Eseguire un'azione
F
su ogni elemento del vettore v. Il vettore non viene modificato.
do_mut<$T>($v: &mut vettore<$T>, $f: |&mut $T|)
Eseguire un'azione
F
su ogni elemento del vettore v. La funzione f/ accetta un riferimento mutabile all'elemento.
mappa<$T, $U>($v: vettore<$T>, $f: |$T| -> $U): vettore<$U>
Mappa il vettore
v
a un nuovo vettore applicando la funzione f a ogni elemento. Mantiene l'ordine degli elementi nel vettore, first è chiamato first.
map_ref<$T, $U>($v: &vettore<$T>, $f: |&$T| -> $U): vettore<$U>
Mappa il vettore
v
a un nuovo vettore applicando la funzione f a ogni elemento. Mantiene l'ordine degli elementi nel vettore, first è chiamato first.
filtro<$T: drop>($v: vettore<$T>, $f: |&$T| -> bool): vettore<$T>
Filtra il vettore
v
applicando la funzione f a ogni elemento. Restituisci un nuovo vettore contenente solo gli elementi per cui f restituisce true.
partizione<$T>($v: vettore<$T>, $f: |&$T| -> bool): (vettore<$T>, vettore<$T>)
Dividi il vettore
v
in due vettori applicando la funzione f a ogni elemento. Restituisci una tupla contenente due vettori: il primo contenente gli elementi per cui f restituisce true, e il secondo contenente gli elementi per cui f restituisce false.
find_index<$T>($v: &vettore<$T>, $f: |&$T| -> bool): Opzione<u64>
Trova l'indice del primo elemento nel vettore
v
che soddisfa il predicato f. Restituisce some(index) se tale elemento viene trovato, altrimenti none().
conteggio<$T>($v: &vettore<$T>, $f: |&$T| -> bool): u64
Conta quanti elementi ci sono nel vettore
v
soddisfa il predicato f.
piega<$T, $Acc>($v: vettore<$T>, $init: $Acc, $f: |$Acc, $T| -> $Acc): $Acc
Ridurre il vettore
v
a un singolo valore applicando la funzione f a ogni elemento. Simile a fold_left in Rust e reduce in Python e JavaScript.
qualsiasi<$T>($v: &vettore<$T>, $f: |&$T| -> bool): bool
Se un elemento nel vettore
v
soddisfa il predicato f. Se il vettore è vuoto, restituisce false.
all<$T>($v: &vettore<$T>, $f: |&$T| -> bool): bool
Se tutti gli elementi nel vettore
v
soddisfa il predicato f. Se il vettore è vuoto, restituisce true.
zip_do<$T1, $T2>($v1: vettore<$T1>, $v2: vettore<$T2>, $f: |$T1, $T2|)
Distrugge due vettori
v1
E
v2
chiamando f a ogni coppia di elementi. Interrompe se i vettori non sono della stessa lunghezza. L'ordine degli elementi nei vettori viene preservato.
zip_do_reverse<$T1, $T2>($v1: vettore<$T1>, $v2: vettore<$T2>, $f: |$T1, $T2|)
Distrugge due vettori
v1
E
v2
chiamando f a ogni coppia di elementi. Interrompe se i vettori non sono della stessa lunghezza. Inizia dalla fine dei vettori.
zip_do_ref<$T1, $T2>($v1: &vettore<$T1>, $v2: &vettore<$T2>, $f: |&$T1, &$T2|)
Iterare attraverso
v1
E
v2
e applica la funzione f ai riferimenti di ogni coppia di elementi. I vettori non vengono modificati. Interrompe se i vettori non sono della stessa lunghezza. L'ordine degli elementi nei vettori viene preservato.
zip_do_mut<$T1, $T2>($v1: &mut vettore<$T1>, $v2: &mut vettore<$T2>, $f: |&mut $T1, &mut $T2|)
Iterare attraverso
v1
e v2 e applica la funzione f ai riferimenti mutabili di ogni coppia di elementi. I vettori possono essere modificati. Interrompe se i vettori non sono della stessa lunghezza. L'ordine degli elementi nei vettori è preservato.
zip_map<$T1, $T2, $U>($v1: vettore<$T1>, $v2: vettore<$T2>, $f: |$T1, $T2| -> $U): vettore<$U>
Distrugge due vettori
v1
E
v2
applicando la funzione f a ogni coppia di elementi. I valori restituiti vengono raccolti in un nuovo vettore. Interrompe se i vettori non sono della stessa lunghezza. L'ordine degli elementi nei vettori viene preservato.
zip_map_ref<$T1, $T2, $U>($v1: &vettore<$T1>, $v2: &vettore<$T2>, $f: |&$T1, &$T2| -> $U): vettore<$U>
Iterare attraverso
v1
e v2 e applica la funzione f ai riferimenti di ogni coppia di elementi. I valori restituiti vengono raccolti in un nuovo vettore. Interrompe se i vettori non sono della stessa lunghezza. L'ordine degli elementi nei vettori viene preservato.
Tabella B: Funzioni macro attualmente disponibili per Option
Aggiungere in anteposizione
divertimento macro pubblico
per ogni riga sottostante:
distruggere<$T>($o: Opzione<$T>, $f: |$T|)
Distruggere
Opzione<T>
e chiamare la chiusura f sul valore al suo interno se ne contiene uno.
do<$T>($o: Opzione<$T>, $f: |$T|)
Distruggere
Opzione<T>
e chiamare la chiusura f sul valore al suo interno se ne contiene uno.
do_ref<$T>($o: &Opzione<$T>, $f: |&$T|)
Esegui una chiusura sul valore interno
T
se ne possiede uno.
do_mut<$T>($o: &mut Opzione<$T>, $f: |&mut $T|)
Esegue una chiusura sul riferimento mutabile al valore all'interno
T
se ne possiede uno.
oppure<$T>($o: Opzione<$T>, $default: Opzione<$T>): Opzione<$T>
Seleziona il primo
Alcuni
valore dalle due opzioni, o None se entrambe sono None. Equivale a a.or(b) di Rust.
e<$T, $U>($o: Opzione<$T>, $f: |$T| -> Opzione<$U>): Opzione<$U>
Se il valore è
Alcuni
, chiama la chiusura f su di essa. Altrimenti, restituisci None. Equivalente a t.and_then(f) di Rust.
and_ref<$T, $U>($o: &Opzione<$T>, $f: |&$T| -> Opzione<$U>): Opzione<$U>
Se il valore è
Alcuni
, chiama la chiusura f su di essa. Altrimenti, restituisci None. Equivalente a t.and_then(f) di Rust.
mappa<$T, $U>($o: Opzione<$T>, $f: |$T| -> $U): Opzione<$U>
Mappa un
Opzione<T>
a Option<U> applicando una funzione a un valore contenuto. Equivalente a t.map(f) di Rust.
map_ref<$T, $U>($o: &Opzione<$T>, $f: |&$T| -> $U): Opzione<$U>
Mappa un
Opzione<T>
valore a Option<U> applicando una funzione a un valore contenuto tramite riferimento. L'Option<T> originale viene preservata. Equivalente a t.map(f) di Rust.
filtro<$T: drop>($o: Opzione<$T>, $f: |&$T| -> bool): Opzione<$T>
Ritorno
Nessuno
se il valore è None, altrimenti restituisce Option<T> se il predicato f restituisce true.
is_some_and<$T>($o: &Opzione<$T>, $f: |&$T| -> bool): bool
Ritorno
falso
se il valore è None, altrimenti restituisce il risultato del predicato f.
distruggi_o<$T>($o: Opzione<$T>, $default: $T): $T
Distruggere
Opzione<T>
e restituire il valore all'interno se ne contiene uno, o default altrimenti. Equivalente a t.unwrap_or(default) di Rust.
Nota: questa funzione è una versione più efficiente di
distruggere_con_default
, poiché non valuta il valore predefinito a meno che non sia necessario. La funzione destroy_with_default dovrebbe essere deprecata in favore di questa funzione.