Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Rust e macro

Rust: scopriamo cosa sono le macro, come si differenziano dalle funzioni del linguaggio e quando conviene usarle nello nostre App
Rust: scopriamo cosa sono le macro, come si differenziano dalle funzioni del linguaggio e quando conviene usarle nello nostre App
Link copiato negli appunti

Sin dai primi esempi affrontati del linguaggio Rust ci siamo abituati alla presenza di uno "strano" punto esclamativo che si presenta all'invocazione di alcune funzionalità.

Ad esempio, se dobbiamo stampare una frase, anche il semplice Hello world iniziale, dobbiamo utilizzare println! con tanto di punto esclamativo prima degli argomenti.

Come abbiamo già avuto modo di ricordare, quel simbolo ci indica che ciò che stiamo invocando non è una comune funzione bensì una macro. In questa lezione, le vedremo nel dettaglio scoprendo cosa sono in realtà, come si differenziano dalle funzioni e quando conviene usarle.

Macro in Rust: di cosa si tratta

La prima domanda che ci si potrebbe porre è: ma perché esistono le macro? Non bastavano le funzioni che sono presenti anche in tanti altri linguaggi di programmazione?

Tra macro e funzioni, esiste una differenza fondamentale. Le macro vengono eseguite a tempo di compilazione, non durante il running del programma. Le macro generano del codice secondo le direttive che noi forniamo. Possono essere considerate una sorta di metaprogrammazione quindi una parte di programmazione che serve a creare altra programmazione!

Ad esempio, in una lezione precedente, abbiamo visto che un vettore può essere inizializzato con la seguente macro a cui passiamo una sequenza di valori:

vec![1, 2, 3]

Eppure sappiamo che un vettore viene normalmente creato con il costrutto Vec::new() e riceve gli elementi con push. La macro vec!, pertanto, può essere vista come un generatore di codice, a tempo di compilazione, che crea il codice in grado di creare un nuovo vettore vuoto e inizializzarlo con gli elementi che le passiamo.

Esistono nello specifico due tipi di macro: le dichiarative e le procedurali. In questa lezione, ci dedicheremo esclusivamente alle prime che ci illustreranno con precisione il ruolo fondamentale svolto dalle macro.

La nostra prima macro in Rust

Andiamo ad inventare una macro, usando un'altra macro per farlo ovvero macro_rules!:

macro_rules! somma {
    ($op1: expr, $op2: expr) => {
        $op1+$op2
    };
}
fn main() {
    let op1=6;
    let op2=8;
    let risultato=somma!(op1,op2);
    println!("{}",risultato)
}

Qui ad esempio ne abbiamo creata una di nome somma che sintetizza l'addizione tra due operatori. Come possiamo osservare, la somma può essere ora eseguita con let risultato=somma!(op1,op2);.

Già dalla sintassi, si percepisce che la macro non è destinata a costituire normale codice da usare in fase di esecuzione e lo si vede da caratteristiche particolari come il dollaro ($) ed il tipo assegnato agli operatori, come per esempio expr. Tecnicamente expr prende il nome di designator e non è certo l'unico infatti, come mostra la documentazione, ne esistono molti altri da scegliere in base alle direttive che vogliamo assegnare alla macro.

Tanto per citare alcuni designator possiamo indicare ident per i nomi di funzioni e variabili, literal per fornire delle costanti, stmt per gli statement e via dicendo.

Il funzionamento dell'overload

Un aspetto molto interessante consiste nel cosiddetto overload, termine che potrebbe non essere del tutto sconosciuto a chi già si è interessato ad altri linguaggi di programmazione. Questa tecnica consiste nella possibilità di fornire ad una medesima macro più combinazioni di argomenti e per ognuna di esse la macro prevederà un comportamento differente.

Ad esempio, supponiamo di essere interessati a creare una macro di nome moltiplicazione che:

  • se riceve 2 argomenti ne restituisce il prodotto;
  • se ne riceve solo uno di default lo restituisce moltiplicato per due.

Come vediamo, la struttura della macro somiglia in questo modo ad una sorta di match:

macro_rules! moltiplicazione {
    ($fattore1: expr, $fattore2: expr) => {
        $fattore1*$fattore2
    };
    ($fattore: expr) => {
        $fattore*2
    };
}
fn main() {
    println!("{}",moltiplicazione!(3,8));
    println!("{}",moltiplicazione!(5));
}

Notiamo che il codice della macro in Rust è diviso in due porzioni. Una contiene delle operazioni da svolgere in risposta a due argomenti, ($fattore1: expr, $fattore2: expr), che rappresenteranno i due operandi della moltiplicazione. La seconda porzione della macro risponde invece ad un unico operando, ($fattore: expr) e come si può vedere restituisce il doppio di quanto fornito.

Alla prova di esecuzione infatti moltiplicazione!(3,8) restituisce 24 ovvero il risultato di 3 per 8 mentre moltiplicazione!(5) restituisce dieci. Trattandosi di una invocazione con un unico operatore ha restituito di default il doppio del numero passato.

Ti consigliamo anche