I tipi associati sono una parte potente del sistema di tipi di Rust. Sono
correlati all'idea di una ‘famiglia di tipi’, in altre parole, raggruppando
insieme più tipi. Questa descrizione è un po' astratta, e quindi tuffiamoci
subito in un esempio. Se si vuole scrivere un tratto Grafo, ci sono due tipi
sui quali essere generici: il tipo dei nodi e il tipo degli archi. Perciò
si potrebbe scrivere un tratto, Grafo<N, A>, così:
trait Grafo<N, A> { fn ha_arco(&self, &N, &N) -> bool; fn archi(&self, &N) -> Vec<A>; // ecc }
Mentre questo in qualche modo funziona, finisce per essere goffo. Per esempio,
ogni funione che vuole prendere un Grafo come parametro adesso ha bisogno
anche di essere generica sui tipi Nodo e Arco:
fn distanza<N, A, G: Grafo<N, A>>(grafo: &G, inizio: &N, fine: &N) -> u32 { ... }
Il nostro calcolo di distanza funziona indipendentemente dal nostro tipo
Arco, e quindi citare la A in questa firma è una distrazione.
Ciò che vogliamo realmente dire è che un certo tipo Arco e un certo tipo
Nodo si mettono insieme per formare ogni tipo di Grafo. Lo possiamo fare
con i tipi associati:
trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; // ecc }
Adesso, i nostri clienti possono usare tutta l'astrazione di un dato Graph:
fn distanza<G: Graph>(grafo: &G, inizio: &G::N, fine: &G::N) -> u32 { ... }
Qui non c'è bisogno di trattare con il tipo Arco!
Analizziamolo in maggiore dettaglio.
Costruiamo quel tratto Grafo. Ecco la definizione:
trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; }
Abbastanza semplice. I tipi associati usano la parola-chiave type, e vanno
dentro il corpo del tratto, insieme alle funzioni.
Queste dichiarazioni type possono avere tutte le cose che hanno le funzioni.
Per esempio, se volessimo che il nostro tipo N implementi Display, così da
poter stampare i nodi, potremmo fare così:
use std::fmt; trait Grafo { type N: fmt::Display; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; }
Proprio come ogni tratto, i tratti che usano tipi associati usano
la parola-chiave impl per fornire implementazioni. Ecco una semplice
implementazione di Grafo:
struct Nodo; struct Arco; struct IlMioGrafo; impl Grafo for IlMioGrafo { type N = Nodo; type A = Arco; fn ha_arco(&self, n1: &Nodo, n2: &Nodo) -> bool { true } fn archi(&self, n: &Nodo) -> Vec<Arco> { Vec::new() } }
Questa implementazione sciocca restituisce sempre true e un Vec<Arco>
vuoto, ma dà un'idea di come implementare questo tipo di cose.
Dapprima ci servono tre
struct, una per il grafo, una per il nodo, e una per l'arco. Se avesse
più senso usare un altro tipo, quello funzionerebbe altrettanto, qui useremo
le struct per tutti e tre.
Poi c'è la riga impl, che è un'implementazione come per qualunque
altro tratto.
Da qui, usiamo = per definire i nostri tipi associati. Il nome usato
dal tratto va sulla sinistra dell'=, e il tipo concreto per cui stiamo
implementando questo va sulla destra. Infine, usiamo i tipi concreti
nelle nostre dichiarazioni di funzione.
C'è un altro pezzo di sintassi di cui dovremmo parlare: gli oggetti-tratto. Se si prova a creare un oggetto-tratto da un tratto con un tipo associato, così:
fn main() { trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; } struct Nodo; struct Arco; struct IlMioGrafo; impl Grafo for IlMioGrafo { type N = Nodo; type A = Arco; fn ha_arco(&self, n1: &Nodo, n2: &Nodo) -> bool { true } fn archi(&self, n: &Nodo) -> Vec<Arco> { Vec::new() } } let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo>; }let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo>;
Otterremo due errori:
error: the value of the associated type `A` (from the trait `main::Grafo`) must
be specified [E0191]
let ogg = Box::new(grafo) as Box<Grafo>;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24:44 error: the value of the associated type `N` (from the trait
`main::Grafo`) must be specified [E0191]
let ogg = Box::new(grafo) as Box<Grafo>;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Non possiamo creare un oggetto-tratto così, perché non conosciamo i tipi associati. Invece, possiamo scrivere così:
fn main() { trait Grafo { type N; type A; fn ha_arco(&self, &Self::N, &Self::N) -> bool; fn archi(&self, &Self::N) -> Vec<Self::A>; } struct Nodo; struct Arco; struct IlMioGrafo; impl Grafo for IlMioGrafo { type N = Nodo; type A = Arco; fn ha_arco(&self, n1: &Nodo, n2: &Nodo) -> bool { true } fn archi(&self, n: &Nodo) -> Vec<Arco> { Vec::new() } } let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo<N=Nodo, A=Arco>>; }let grafo = IlMioGrafo; let ogg = Box::new(grafo) as Box<Grafo<N=Nodo, A=Arco>>;
La sintassi N=Nodo ci permette di fornire un tipo concreto, Nodo, per
il parametro di tipo N. Lo stesso con A=Arco. Se non fornissimo questo
vincolo, non potremmo essere sicuri di quale impl corrisponda a questo
oggetto-tratto.