Nom de la technique pour déduire les arguments de type d'un paramètre de type?

9

Configuration: Supposons que nous avons un type appelé Iteratorqui a un paramètre de type Element:

interface Iterator<Element> {}

Ensuite, nous avons une interface Iterablequi a une méthode qui retournera un Iterator.

// T has an upper bound of Iterator
interface Iterable<T: Iterator> {
    getIterator(): T
}

Le problème d' Iteratorêtre générique est que nous devons lui fournir des arguments de type.

Une idée pour résoudre ce problème consiste à "déduire" le type de l'itérateur. Le pseudo-code suivant exprime l'idée qu'il existe une variable de type Elementqui est supposée être l'argument de type Iterator:

interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}

Et puis nous l'utilisons quelque part comme ceci:

class Vec<Element> implements Iterable<VecIterator<Element>> {/*...*/}

Cette définition de Iterablen'utilise Elementnulle part ailleurs dans sa définition mais mon vrai cas d'utilisation le fait. Certaines fonctions qui utilisent Iterabledoivent également pouvoir contraindre leurs paramètres à accepter des Iterables qui ne renvoient que certains types d'itérateurs, comme un itérateur bidirectionnel, c'est pourquoi l'itérateur renvoyé est paramétré au lieu du simple type d'élément.


Des questions:

  • Existe-t-il un nom établi pour ces variables de type inférées? Et la technique dans son ensemble? Ne pas connaître la nomenclature spécifique a rendu difficile la recherche de ces exemples dans la nature ou en savoir plus sur les fonctionnalités spécifiques à la langue.
  • Toutes les langues avec des génériques n'ont pas cette technique; existe-t-il des noms pour des techniques similaires dans ces langues?
Levi Morrison
la source
1
Pouvez-vous montrer du code qui ne compile pas que vous souhaitez compiler? Je pense que cela clarifiera la question.
Sweeper
1
Vous pouvez également choisir une langue (ou dire quelle langue vous utilisez et ce que signifie la syntaxe). Ce n'est évidemment pas, par exemple, C #. Il y a beaucoup d'informations sur "l'inférence de type" disponibles sur Internet, mais je ne suis pas sûr que cela s'applique ici.
5
J'implémente des génériques dans un langage, je n'essaye pas d'obtenir du code dans un langage à compiler. C'est aussi une question de nom et de design. C'est pourquoi c'est quelque peu agnostique. Sans connaître les termes, il est difficile de trouver des exemples et de la documentation dans les langues existantes. Ce n'est sûrement pas un problème unique?
Levi Morrison

Réponses:

2

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 Iterableinterface comme:

interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}

Cela donne aux utilisateurs de l'interface une puissance maximale car ils obtiennent le type Tde 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 Traitsyntaxe 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' Iterableinterface 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
}

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 Elementdonc c'est Tun 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 Iterablen'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' Iterableinterface 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).

amon
la source