Rust - Le ennuple (tuple)


Argomento di questo paragrafo è un costrutto utile e molto utilizzato anche se a volte passa un po', come dire, sottotraccia, nel senso che si sente parlare spesso di array, stringhe, slice ecc... e più raramente  delle ennuple (spesso indicate anche in italiano con il termine inglese tuple o qualche volta anche n-uple. Io in questo paragrafo adopererò la definizione nella nostra lingua).
Iniziamo a dare una definizione formale, ne troverete di molto simili sul web:

Una ennupla è una collezione ordinata di valori di tipi potenzialmente diversi, racchiusi tra parentesi tonde.
Il tipo standard di una tupla è della forma:
(T1,T2,,Tn)

Un'altra definizione è:
una ennupla è una collezione di valori di tipi anche diversi raccolti in un unico tipo composto

Abbiamo già visto in azione una ennupla? Certo che si, basti ricordare la inizializzazione multipla nel capitolo relativo alle variabili e costanti. Avevamo visto questa definizione formale:
let (var-1, var-2, var-3... var-n) = (valore-1, valore-2, valore-3,... valore-n)
specificando che a destra dell'operatore di assegnazione c'era una ennupla, evidenziata.

In realtà per definire una nuova ennupla abbiamo due modi, come avrete intuitol, lasciando fare all'inferenza oppure no:

let enumnome : (tipo-1, tipo-2,..., tipo-n) = (valore-1, valore-2, ..., valore-n);
let enumnome = (valore-1, valore-2, ..., valore-n);

Trattandosi di collezione ordinata c'è una indicizzazione, come di consueto per questi tipi, ne vedremo altri, tale indicizzazione inizia da 0 e prosegue sequenzialmente.
Anche le ennuple sono per natura immutabili ed è necessario inserire il solito mut per renderle modificabili, niente di nuovo.
Vediamo un picclo esempio di base:

Esempio 12.1

fn main() {
    let t01 = (1, 'a', "aaa");
    println!("{:?}", t01);
}

questo semplice codice espone una tuple nel suo completo sviluppo

(1, 'a', "aaa")

Da notare che una ennupla vuota viene chiamata tipo unit, appunto indicato come (). Quest'ultimo vi ricorda qualcosa? Ma certo, guardate come definiamo ogni volta l'entry point (main) dei nostri programmi, riga 1 dell'esempio 12.1, tanto per dire l'ultimo caso. Troviamo una ennupla in fondo e non è affatto un caso, come vedremo studiando le funzioni.
Una tupla con un solo elemento deve comunque contenere la virgola:
(8,) è una tupla con un elemento
(8) è il valore 8 e basta

Le ennuple si distinguono per numero di elementi e per il loro tipo in conseguenza di ciò ad eempio:

(i32, char)
è diversa da
(char, i32)

la lunghezza n, ovvero il numero di elementi che compone una tuple è detta "arità".
Va stabilito in maniera forte e chiara da subito che le ennuple non sono array (struttura molto più flessibile che vedremo in altro capitolo) e nemmeno ne sono loro validi sostituti. E neanche sono struct.  Quindi non utilizzateli in questo senso. Sono dei contenitori che possono risultare utilissimi in molte situazioni, ma, ripeto, non sono strutture dati maneggevoli. In particolare quando le useremo:

Quindi, in quali casi possiamo favorevolmente utilizzare questo costrutto? Principlamente in queste situazioni
--- i valori sono pochi e logicamente collegati;
--- l’uso è locale e non serve documentare i campi;
--- Restituire più valori da una funzione.
--- Passare un gruppo di valori a una funzione con un singolo parametro.
--- Creare piccole strutture dati senza la necessità di definire una struct.
--- Inizializzazione multipla di più variabili Come abbiano visto. In quel caso operiamo una destrutturazione della ennupla.

Per natura le ennuple sono strutture rigide e la loro manipolazione non è particolamente flessibile. Ad esempio la loro dimensione è fissa e quindi non è possibile nè aggiungere nè togliere elementi. Ovviamente se volete sostituire un elemento dovete immetterne un altro dello stesso tipo. Vediamo un esempio, premettendo che l'accesso al singolo dato di una tuple si ha con il formato:

nometuple.indice
Esempio 15.2

fn main() {
     let mut t01 = (1, 'a', "aaa");
     println!("{:?}", t01);
     t01.1 = 'b';
     println!("{:?}", t01);
 }

che ci restituisce:

(1, 'a', "aaa")
(1, 'b', "aaa")

se però provassimo a sostituire l'istruzione alla riga 4 come segue:

t01.1 = 3;

il compilatore risponde come si deve:

error[E0308]: mismatched types
--> r148.rs:4:14
|
4 | t01.1 = 3;
| ----- ^ expected `char`, found `u8`
| |
| expected due to the type of this binding

Non funziona pertanto l'operatore [ ], come avviene invece per le stringhe e ciò forse avvicinerebbe le ennuple alle struct, più che alle sequenze. Insomma le ennuple sono un po' varie come natura.
Avrete certo notato che per accedere agli elementi di una ennupla abbiamo usato indici numerici espliciti preceduti dal punto. Quella è l'unica strada permessa, l’indice deve essere un literal numerico, non un’espressione. Anche una costante non è ammessa. Inoltre l'indice deve essere noto a compile time. Anche questa è una limitazione abbastanza pesante. Ad esempio non potrete usare una normale iterazione sugli elementi di una ennupla. E' inoltre parecchio complicato anche estrarre il numero degli elementi al suo interno. Se però vi state facendo queste domande, ovvero come iterare sugli elementi o recuperarne il numero, è molto probabile che stiate usando la struttura sbagliata per conservare i vostri dati.
Un metodo pratico, se proprio vi dovesse servire contare gli elementi di una ennupla e questa ha tutti gli elementi dello stesso tipo, è creare un vettore a partire da essa e usare i metodi tipici per i vettori stessi:

fn main() {
  let my_tuple = (1, 2, 3);
  let my_vector: Vec<_> = vec![my_tuple.0, my_tuple.1, my_tuple.2];
  let tuple_length = my_vector.len();
  println!("La lunghezza della tupla è: {}", tuple_length);
}

se volete, potete rivedere questo codice dopo che avremo affrontato i vettori. Ma ovviamente la sua utilità è comunque molto limitata. Sul Web troverete molte soluzioni nessuna delle quali definitiva, almeno tra quelle che ho trovato io. Francamente non è un problema su cui perdere troppo tempo.

Un'altra limitazione delle ennuple è che esse supportano certi trait fino ad una arità pari a 12. Ad esempio il banale println! su una ennupla di 13 elementi

let t01 = (1,2,3,4,5,6,7,8,9,0,1,2,3);
println!("{:?}", t01)


restituisce questo verboso responso in compilazione:

error[E0277]: `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` doesn't implement `Debug`
--> r170.rs:3:18
3 | println!("{:?}", t01);
| ---- ^^^ `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` cannot be formatted using `{:?}` because it doesn't implement `Debug`
| required by this formatting parameter
= help: the trait `Debug` is not implemented for `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})`
= help: the following other types implement trait `Debug`:
()
(A, Z, Y, X, W, V, U, T)
(B, A, Z, Y, X, W, V, U, T)
(C, B, A, Z, Y, X, W, V, U, T)
(D, C, B, A, Z, Y, X, W, V, U, T)
(E, D, C, B, A, Z, Y, X, W, V, U, T)
(T,)
(U, T)
and 5 others
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.

Che dice fin troppo per i nostri scopi fornedoci una spiegazione completa.
In assoluto le ennuple limitate a 12 elementi supportano i seguenti trait:

PartialEq
Eq
PartialOrd
Ord
Debug
Default
Hash

mentre per tutti valgono questi:

Clone
Copy
Send
Sync
Unpin
UnwindSafe
RefUnwindSafe

Riassumendo, Il limite di 12 elementi riguarda solo le implementazioni automatiche nella std. In futuro potrebbe cambiare.
Tra tutti per i nostri scopi è interessante il supporto al trait Copy e al trait Clone cosa che ci fa capire che possiamo effettuare delle copie di una tuple. Detto che clone è sempre disponibile (in realtà se la ennupla è copy allora i due netodi sono praticamwnte coincidenti) qualora gli elementi interni supportino Copy è possibile utilizzare quel trait per la copia. In entrambi i casi avremo copie indipendenti ma in linea di massima personalmente userei clone soltanto laddove non sia disponibile copy che a mio avviso è istruzione più chiara e idiomatica. A dire il vero sul web ho trovato alcuni utenti che affermano che clone sia più efficiente. Non effettuato test in merito. Comunque, per esemplificare:

let a = (10, 20);
let b = a; // bitwise copy
let c = (String::from("ciao"), vec![1,2,3]);
let d = c.clone();

Evidenziamo il fatto che copy effettua usa una copia bit a bit.

Di seguito un esempio  per illustrare una volta di più il rigido sistema di regole di borrowing e ownership di questo linguaggio applicato alle ennuple basandoci sulla possibilità di inizializzare più variabili contemporaneamente.

Esempio 15.3

fn main() {
    let t01 = ("uno".to_string(), "due".to_string());
    println!("{:?}", t01);
    let (a,b) = t01;
    println!("{}", a);
    println!("{:?}", t01);
}

Questo codice non compila. Infatti il compilatore ci dice:

error[E0382]: borrow of partially moved value: `t01`
--> r172.rs:6:22
4 | let (a,b) = t01;
| - value partially moved here
5 | println!("{}", a);
6 | println!("{:?}", t01);
| ^^^ value borrowed here after partial move
= note: partial move occurs because `t01.1` has type `String`, which does not implement the `Copy` trait
help: borrow this binding in the pattern to avoid moving the value
4 | let (a,ref b) = t01;
| +++

Esatto. L'operazione di assegnazione muove gli elementi della ennupla fuori dal controllo della  stessa, perchè abbiamo a che fare con delle string che non implementano copy. Quindi quando il controllo passa alle variabili viene sottratto alla ennupla. Interessante la soluzione proposta dal compilatore. In pratica se la riga
let(a, b) = t01; diventa
let(ref a, ref b) = t01;
tutto funziona. Questo perchè ref è un modificatore di binding che crea un legame per riferimento creando quindi un borrow. In questo modo le cose tornano a posto.

Un piccolo trucco, banale in realtà, ancora nel caso di inizializzazione di più variabili. Se la ennupla che volete abbinare alle variabili è composta da n elementi e le variabili sono n-1 dovete la variabile di scarto _ ad esempio:

let t01 = (1,2,3,4);
let (a,_,c) = t01;

Con le ennuple ci rivedremo quando parleremo delle funzioni. E lì sarà casa loro.