Je ne sais pas s'il existe un terme particulier pour ce problème, mais il existe trois classes générales de solutions:
- éviter les types concrets au profit d'une répartition dynamique
- autoriser les paramètres de type d'espace réservé dans les contraintes de type
- éviter les paramètres de type en utilisant des types / familles de types associés
Et bien sûr, la solution par défaut: continuez à énoncer tous ces paramètres.
Évitez les types de béton.
Vous avez défini une Iterable
interface comme:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
Cela donne aux utilisateurs de l'interface une puissance maximale car ils obtiennent le type T
de béton exact de l'itérateur. Cela permet également à un compilateur d'appliquer plus d'optimisations telles que l'inline.
Cependant, s'il Iterator<E>
s'agit d'une interface distribuée dynamiquement, il n'est pas nécessaire de connaître le type concret. C'est par exemple la solution que Java utilise. L'interface serait alors écrite comme:
interface Iterable<Element> {
getIterator(): Iterator<Element>
}
Une variante intéressante de ceci est la impl Trait
syntaxe de Rust qui vous permet de déclarer la fonction avec un type de retour abstrait, mais sachant que le type concret sera connu sur le site d'appel (permettant ainsi des optimisations). Cela se comporte de manière similaire à un paramètre de type implicite.
Autoriser les paramètres de type d'espace réservé.
L' Iterable
interface n'a pas besoin de connaître le type d'élément, il peut donc être possible d'écrire ceci comme:
interface Iterable<T: Iterator<_>> {
getIterator(): T
}
Où T: Iterator<_>
exprime la contrainte «T est n'importe quel itérateur, quel que soit le type d'élément». Plus rigoureusement, nous pouvons exprimer cela comme: «il existe un type Element
donc c'est T
un Iterator<Element>
», sans avoir à connaître de type concret pour Element
. Cela signifie que l'expression de type Iterator<_>
ne décrit pas un type réel et ne peut être utilisée que comme contrainte de type.
Utilisez des familles de types / types associés.
Par exemple, en C ++, un type peut avoir des membres de type. Ceci est couramment utilisé dans toute la bibliothèque standard, par exemple std::vector::value_type
. Cela ne résout pas vraiment le problème des paramètres de type dans tous les scénarios, mais comme un type peut faire référence à d'autres types, un seul paramètre de type peut décrire toute une famille de types liés.
Définissons:
interface Iterator {
type ElementType
fn next(): ElementType
}
interface Iterable {
type IteratorType: Iterator
fn getIterator(): IteratorType
}
Alors:
class Vec<Element> implement Iterable {
type IteratorType = VecIterator<Element>
fn getIterator(): IteratorType { ... }
}
class VecIterator<T> implements Iterator {
type ElementType = T
fn next(): ElementType { ... }
}
Cela semble très flexible, mais notez que cela peut rendre plus difficile l'expression des contraintes de type. Par exemple, tel qu'écrit Iterable
n'impose aucun type d'élément d'itérateur, et nous pourrions vouloir déclarer à la interface Iterator<T>
place. Et vous avez maintenant affaire à un calcul de type assez complexe. Il est très facile de rendre accidentellement un tel système de type indécidable (ou peut-être qu'il l'est déjà?).
Notez que les types associés peuvent être très pratiques comme valeurs par défaut pour les paramètres de type. Par exemple, en supposant que l' Iterable
interface a besoin d'un paramètre de type distinct pour le type d'élément qui est généralement mais pas toujours le même que le type d'élément itérateur, et que nous avons des paramètres de type d'espace réservé, il pourrait être possible de dire:
interface Iterable<T: Iterator<_>, Element = T::Element> {
...
}
Cependant, ce n'est qu'une fonctionnalité d'ergonomie linguistique et ne rend pas la langue plus puissante.
Les systèmes de type sont difficiles, il est donc bon de regarder ce qui fonctionne et ne fonctionne pas dans d'autres langues.
Par exemple, pensez à lire le chapitre Advanced Traits du Rust Book, qui traite des types associés. Mais notez que certains points en faveur des types associés au lieu des génériques ne s'appliquent que là car le langage ne comporte pas de sous-typage et chaque trait ne peut être implémenté au plus qu'une fois par type. C'est-à-dire que les traits Rust ne sont pas des interfaces de type Java.
D'autres systèmes de types intéressants incluent Haskell avec différentes extensions de langage. Les modules / foncteurs OCaml sont une version relativement simple des familles de types, sans les entremêler directement avec des objets ou des types paramétrés. Java se distingue par les limitations de son système de types, par exemple les génériques avec effacement de type, et aucun générique par rapport aux types de valeur. C # est très semblable à Java mais parvient à éviter la plupart de ces limitations, au prix d'une complexité d'implémentation accrue. Scala essaie d'intégrer des génériques de style C # avec des classes de style Haskell au-dessus de la plate-forme Java. Les modèles trompeusement simples de C ++ sont bien étudiés mais ne sont pas comme la plupart des implémentations génériques.
Il vaut également la peine de regarder les bibliothèques standard de ces langages (en particulier les collections de bibliothèques standard comme les listes ou les tables de hachage) pour voir quels modèles sont couramment utilisés. Par exemple, C ++ possède un système complexe de différentes capacités d'itérateur, et Scala code les capacités de collecte à grain fin en tant que traits. Les interfaces de bibliothèque standard Java sont parfois saines, par exemple Iterator#remove()
, mais peuvent utiliser des classes imbriquées comme une sorte de type associé (par exemple Map.Entry
).