Non so se esiste un termine particolare per questo problema, ma ci sono tre classi generali di soluzioni:
- evitare tipi concreti a favore della spedizione dinamica
- consentire i parametri di tipo segnaposto nei vincoli di tipo
- evitare i parametri di tipo utilizzando tipi / famiglie di tipi associati
E ovviamente la soluzione predefinita: continua a sillabare tutti quei parametri.
Evita i tipi concreti.
Hai definito Iterable
un'interfaccia come:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
Ciò offre agli utenti dell'interfaccia la massima potenza perché ottengono l'esatto tipo concreto T
dell'iteratore. Ciò consente inoltre a un compilatore di applicare più ottimizzazioni come l'inline.
Tuttavia, se si Iterator<E>
tratta di un'interfaccia spedita in modo dinamico, non è necessario conoscere il tipo di calcestruzzo. Questa è ad esempio la soluzione utilizzata da Java. L'interfaccia verrebbe quindi scritta come:
interface Iterable<Element> {
getIterator(): Iterator<Element>
}
Una variante interessante di questo è la impl Trait
sintassi di Rust che consente di dichiarare la funzione con un tipo di ritorno astratto, ma sapendo che il tipo concreto sarà noto nel sito di chiamata (consentendo quindi ottimizzazioni). Ciò si comporta in modo simile a un parametro di tipo implicito.
Consenti parametri del tipo di segnaposto.
L' Iterable
interfaccia non ha bisogno di conoscere il tipo di elemento, quindi potrebbe essere possibile scrivere questo come:
interface Iterable<T: Iterator<_>> {
getIterator(): T
}
Dove T: Iterator<_>
esprime il vincolo "T è un iteratore, indipendentemente dal tipo di elemento". Più rigorosamente, possiamo esprimerlo come: "esiste un tipo in Element
modo che T
sia un Iterator<Element>
", senza dover conoscere alcun tipo concreto per Element
. Ciò significa che l'espressione di tipo Iterator<_>
non descrive un tipo reale e può essere utilizzata solo come vincolo di tipo.
Usa famiglie di tipi / tipi associati.
Ad esempio in C ++, un tipo può avere membri del tipo. Questo è comunemente usato in tutta la libreria standard, ad es std::vector::value_type
. Questo non risolve davvero il problema dei parametri di tipo in tutti gli scenari, ma poiché un tipo può fare riferimento ad altri tipi, un singolo parametro di tipo può descrivere un'intera famiglia di tipi correlati.
Definiamo:
interface Iterator {
type ElementType
fn next(): ElementType
}
interface Iterable {
type IteratorType: Iterator
fn getIterator(): IteratorType
}
Poi:
class Vec<Element> implement Iterable {
type IteratorType = VecIterator<Element>
fn getIterator(): IteratorType { ... }
}
class VecIterator<T> implements Iterator {
type ElementType = T
fn next(): ElementType { ... }
}
Sembra molto flessibile, ma nota che ciò può rendere più difficile esprimere i vincoli di tipo. Ad esempio, come scritto Iterable
non impone alcun tipo di elemento iteratore e potremmo voler dichiarare interface Iterator<T>
invece. E ora hai a che fare con un calcolo di tipo abbastanza complesso. È molto facile rendere indecidibile un sistema di questo tipo (o forse lo è già?).
Si noti che i tipi associati possono essere molto utili come valori predefiniti per i parametri di tipo. Ad esempio, supponendo che l' Iterable
interfaccia abbia bisogno di un parametro di tipo separato per il tipo di elemento che di solito è ma non sempre uguale al tipo di elemento iteratore e che abbiamo parametri di tipo segnaposto, potrebbe essere possibile dire:
interface Iterable<T: Iterator<_>, Element = T::Element> {
...
}
Tuttavia, questa è solo una funzione di ergonomia della lingua e non rende la lingua più potente.
I sistemi di tipi sono difficili, quindi è bene dare un'occhiata a cosa funziona e non funziona in altre lingue.
Ad esempio, considera di leggere il capitolo Tratti avanzati nel Rust Book, che discute i tipi associati. Tuttavia, tieni presente che alcuni punti a favore di tipi associati anziché generici si applicano solo perché la lingua non presenta sottotipi e ogni tratto può essere implementato al massimo una sola volta per tipo. I tratti di Rust non sono interfacce simili a Java.
Altri sistemi di tipo interessanti includono Haskell con varie estensioni di lingua. I moduli / funzioni OCaml sono una versione relativamente semplice delle famiglie di tipi, senza mescolarle direttamente con oggetti o tipi parametrizzati. Java si distingue per le limitazioni nel suo sistema di tipi, ad es. Generici con cancellazione del tipo e nessun generico rispetto ai tipi di valore. C # è molto simile a Java ma riesce a evitare la maggior parte di questi limiti, a costo di una maggiore complessità dell'implementazione. Scala cerca di integrare i generici in stile C # con le macchine da scrivere in stile Haskell sulla piattaforma Java. I modelli ingannevolmente semplici di C ++ sono ben studiati ma sono diversi dalla maggior parte delle implementazioni generiche.
Vale anche la pena guardare le librerie standard di questi linguaggi (in particolare le raccolte di librerie standard come elenchi o tabelle hash) per vedere quali schemi sono comunemente usati. Ad esempio, C ++ ha un sistema complesso di diverse funzionalità di iteratore e Scala codifica le funzionalità di raccolta a grana fine come tratti. Le interfacce della libreria standard Java a volte non sono corrette, ad esempio Iterator#remove()
, ma possono utilizzare le classi nidificate come un tipo di tipo associato (ad esempio Map.Entry
).