Rust - Gli enumeratori


Altro interessante argomento relativo ai tipi definiti dall'utente è quello degli enumeratori. Si tratta di un argomento decisamente importante tanto che il linguaggio, come vedremo, prevede, ad esempio, un paio di enumeratori fondamentali ovvero Option e Result, ma in generale essi sono ben presenti. In pratica, permettono di definire un tipo che può assumere una tra diversi possibili varianti. Ogni variante, che è proprio il nome di ciascuno degli elementi interni, dell'enumeratore, può essere semplice, senza dati, oppure può portare con sé ulteriori dati. In parole povere, è un tipo definito dal programmatore che permette di selezionare un valore da un elenco.
Diamo la solita definizione formale che si basa sulla keyword enum:

enum Identificatore {
  Variante-1,
  Variante-2,
  .....,
  Variante-n,
}

gli identificatori sia per l'enumeratore che per le varianti, che sono separate tra di loro dalla virgola, dovrebbero seguire le consute regole CamelCase. I tipi abbinati alle singole varianti possono essere diversi tra loro, questo è importante, in conseguenza di ciò la varietà di situazioni che si presentano quando definite un enumeratore possono essere davvero tante. La definizione di un enumeratore tuttavia può essere più complessa in quanto ogni variante può accettare:

 vediamo un possibile esempio riassuntivo:

enum Mouse {
  Assente,
  Peso(u8),
  Posizione{x: i32, y: i32},
  Colore(String),
}

quindi abbiamo un primo campo che presenta solo un identificatore, un secondo che propone un tipo, un terzo che inserisce anche un nome per il tipo proposto e un quarto che presenta un altro tipo. Come si vede c'è varietà assoluta e questo è uno dei punti di forza degli enumeratori. Come le struct anche gli enumeratori sono sostnzialmente dei tipi creati dal programmatore e quindi devono essere utilizzati in qualche modo. Vediamo un esempio assolutamente banale che però fa uso del pattern matching, ahimè con gli enumeratori è quasi impossibile farne a meno (anzi sono i compagni di viaggio perfetti) se li si vogliono utilizzare appieno, comunque anche se non abbiamo ancora parlato di pattern matching direi che si può già intuire il funzionamento:

Esempio 16.1

enum Saluto {
Arrivo(String),
Partenza(String),
}

fn main() {
    let messaggio = Saluto::Arrivo("Ciao!".to_string());
    match messaggio {
        Saluto::Arrivo(s) => println!("Messaggio di arrivo: {}", s),
        Saluto::Partenza(s) => println!("Messaggio di partenza: {}", s),
    }
}

Fa la sua comparsa l'operatore :: (double colon) che ci permette di accedere alle varianti di un enumeratore.
Un altro esempio, classico, lo troverete molto simile su parecchi testi e siti (vi prego di ignorare i numerosi warning):

Esempio 16.2

#[derive(Debug)]
enum Colore {
    Rosso,
    Giallo,
    Verde,
}

fn main(){
    let rosso = Colore::Rosso;
    let giallo = Colore::Giallo;
    let verde = Colore::Verde;
    println!("{:?}", rosso);
}

Ricordo che l’istruzione #[derive(Debug)] è un attributo che si applica a strutture o enumerazioni per auto-generare automaticamente l’implementazione del trait Debug. Questo trait permette di stampare le informazioni di una struttura in un formato utile per il debugging, utilizzando il formattatore {:?}. ma dei formattatori riparleremo.
Come sempre, è possibile definire la mutabilità degli elelementi che create partendo da un enumeratore, naturalmente ci vuole il consueto mut prendendo in considerazione l'esempio 16.2 potremmo modificare come segue, vi lascio il piacere della prova:
let mut rosso = Colore::Rosso;
rosso = Colore::Giallo
;
Ora, visto che degli enumeratori trovo difficile parlare senza esempi, vediamo un super-classico che troverete ovunque che ci mostra la cooperazione con il pattern matching (e vedremo anche if-let tra qualche paragrafo) e intanto mettiamoci dentro un ricordo dei mitici 883. Anche qui ignorate i warning:

Esempio 18.3

enum Direzione {
    Nord,
    Sud,
    Ovest,
    Est,
}

fn main() {
    let d = Direzione::Nord;
    match d {
        Direzione::Nord = > println!("Vai a nord"),
        Direzione::Sud = > println!("Vai a sud"),
        Direzione::Est = > println!("Vai a est"),
        Direzione::Ovest = > println!("Vai a ovest"),
    }
}

La copertura dei casi deve essere completa diversamente vi prenderete un errore di
non-exhaustive patterns.

Vediamo adesso un esempio in cui facciamo uso di tipi preimpostati:

Esempio 18.4

enum Messaggio {
    Quit,
    Move(i32, i32),
    Write(String),
}

fn main() {
let m = Messaggio::Move(10, 20);
match m {
    Messaggio::Quit = > println!("Stop"),
    Messaggio::Move(x, y) = > println!("Muovi a ({x}, {y})"),
    Messaggio::Write(s) = > println!("Scrivi: {s}"),
}

let m2 = Messaggio::Write(String::from("Ciao"));
match m2 {
    Messaggio::Quit = > println!("Stop"),
    Messaggio::Move(x, y) = > println!("Muovi a ({x}, {y})"),
    Messaggio::Write(s) = > println!("Scrivi: {s}"),
}
}

Vi lascio il semplice caso in cui viene utilizzato il primo ramo, quello dove trovate Quit.

Come sempre, mi piace affrontare il problema della copia. Le regole sono sempre le stesse. Se tutte le varianti sono Copy lo è anche l'enumeratore e qui non ci sono problemi.

#[derive(Copy, Clone)]
enum Direzione {
  Nord,
  Sud,
  Est,
  Ovest,
}

let a = Direzione::Nord;
let b = a; // copia bitwise

qui non ci sono problemi le 4 varianti sono discriminanti che non danno problemi. La copia avviene senza problemi.
Il caso critico avviene in una situazione simile:

enum Messaggio {
  Testo(String),
  Numero(i32),
}

String non è Copy quindi nemmeno l'enumeratore Messaggio può esserlo. Quindi:

let m1 = Messaggio::Testo("ciao".to_string());
let m2 = m1; //  move, non copia

In questi casi la soluzione è clone():

let m2 = m1.clone(); // deep copy del buffer della String

E' possibile, qualora si voglia evitare la copia e ci si debba affidare solo alla lettura usare il borrowing:

let m2 = &m1;

Un altro discorso riguarda la possibilità di implementazione della copia per via interna, ma è un discorso da affrontare dopo aver parlato delle funzioni. 

Per ora è tutto ma mancano alcuni pezzi. Ad esempio ritroveremo gli enumeratori al lavoro insieme alle funzioni, l'avrete già capito è argomento costante con i vari costrutti. E poi esplicitata la possibilità, accennata poco fa, di accettare a loro volta delle funzioni all'interno, cosa non comune in altri linguaggi. Option, Result... c'è tanta carne al fuoco per gli enumeratori.