Non so se c'è un termine particolare per questo problema, ma ci sono tre classi generali di soluzioni:
- evita tipi concreti a favore dell'invio dinamico
- consente di impostare i parametri del tipo segnaposto nei vincoli di tipo
- evitare i parametri di tipo usando tipi / tipi di famiglie associati
E ovviamente la soluzione predefinita: continua a scrivere tutti quei parametri.
Evita tipi di calcestruzzo.
Hai definito un'interfaccia Iterable
come:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
Questo fornisce agli utenti dell'interfaccia potenza massima in quanto ottengono il tipo concreto esatto T
dell'iteratore. Ciò consente anche a un compilatore di applicare più ottimizzazioni come l'inlining.
Tuttavia, se Iterator<E>
è un'interfaccia inviata dinamicamente, non è necessario conoscere il tipo concreto. Questo è ad es. la soluzione utilizzata da Java. L'interfaccia verrebbe quindi scritta come:
interface Iterable<Element> {
getIterator(): Iterator<Element>
}
Una variante interessante di questo è la sintassi di impl Trait
di Rust che ti permette di dichiarare la funzione con un tipo di ritorno astratto, ma sapendo che il tipo concreto sarà conosciuto nel sito di chiamata (consentendo così ottimizzazioni). Ciò si comporta in modo simile a un parametro di tipo implicito.
Consenti parametri tipo segnaposto.
L'interfaccia Iterable
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 qualsiasi iteratore, indipendentemente dal tipo di elemento". Più rigorosamente, possiamo esprimere questo come: "esiste qualche tipo Element
in modo che T
sia un Iterator<Element>
", senza dover conoscere alcun tipo di calcestruzzo per Element
. Ciò significa che l'espressione di tipo Iterator<_>
non descrive un tipo effettivo e può essere utilizzata solo come un vincolo di tipo.
Utilizza famiglie di tipi / tipi associati.
es. in C ++, un tipo può avere membri di tipo. Questo è comunemente usato in tutta la libreria standard, ad es. %codice%. Questo in realtà non risolve il problema del parametro di tipo in tutti gli scenari, ma poiché un tipo può riferirsi 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
}
Quindi:
class Vec<Element> implement Iterable {
type IteratorType = VecIterator<Element>
fn getIterator(): IteratorType { ... }
}
class VecIterator<T> implements Iterator {
type ElementType = T
fn next(): ElementType { ... }
}
Questo sembra molto flessibile, ma si noti che questo può rendere più difficile esprimere vincoli di tipo. Per esempio. come scritto std::vector::value_type
non impone alcun tipo di elemento iteratore, e potremmo invece voler dichiarare Iterable
. E ora hai a che fare con un calcolo di tipo abbastanza complesso. È molto facile rendere un sistema di questo tipo indecidibile (o forse lo è già?).
Si noti che i tipi associati possono essere molto convenienti come valori predefiniti per i parametri del tipo. Per esempio. partendo dal presupposto che l'interfaccia interface Iterator<T>
abbia bisogno di un parametro di tipo separato per il tipo di elemento che è solitamente ma non sempre uguale al tipo di elemento iteratore e che abbiamo parametri di tipo segnaposto, è possibile dire:
interface Iterable<T: Iterator<_>, Element = T::Element> {
...
}
Tuttavia, questa è solo una funzione di ergonomia del linguaggio e non rende il linguaggio più potente.
I sistemi di tipo sono difficili, quindi è bene dare un'occhiata a cosa funziona e cosa non funziona in altre lingue.
es. considera la lettura del capitolo Tratti avanzati nel Rust Book, che discute i tipi associati. Ma si noti che alcuni punti a favore dei tipi associati invece dei generici si applicano solo in quanto la lingua non presenta sottotipi e ogni tratto può essere implementato al massimo una volta per tipo. Cioè I tratti ruggine non sono interfacce tipo Java.
Altri sistemi di tipi interessanti includono Haskell con varie estensioni di lingua. OCaml modules / functors sono una versione relativamente semplice di famiglie di tipi, senza mescolarli direttamente con oggetti o tipi parametrizzati. Java è notevole per le limitazioni nel suo sistema di tipi, ad es. generici con cancellazione dei tipi e niente generici su tipi di valore. C # è molto simile a Java ma riesce a evitare la maggior parte di queste limitazioni, a costo di una maggiore complessità di implementazione. Scala cerca di integrare i generici in stile C # con i typeclass 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 raccolte di librerie standard come elenchi o tabelle hash) per vedere quali modelli sono comunemente usati. Per esempio. C ++ ha un sistema complesso di diverse capacità di iteratore e Scala codifica le funzionalità di raccolta a grana fine come tratti. Le interfacce della libreria standard Java sono talvolta non corrette, ad es. Iterable
, ma è possibile utilizzare classi nidificate come tipo di tipo associato (ad esempio Iterator#remove()
).