Comme personne d'autre n'a répondu à la question, je pense que je vais essayer moi-même. Je vais devoir devenir un peu philosophique.
La programmation générique concerne l'abstraction sur des types similaires, sans perte d'informations de type (ce qui se produit avec le polymorphisme de valeur orienté objet). Pour ce faire, les types doivent nécessairement partager une sorte d'interface (un ensemble d'opérations, pas le terme OO) que vous pouvez utiliser.
Dans les langages orientés objet, les types satisfont une interface grâce aux classes. Chaque classe a sa propre interface, définie comme faisant partie de son type. Étant donné que toutes les classes List<T>
partagent la même interface, vous pouvez écrire du code qui fonctionne quel que soit T
votre choix. Une autre façon d'imposer une interface est une contrainte d'héritage, et bien que les deux semblent différentes, elles sont en quelque sorte similaires si vous y réfléchissez.
Dans la plupart des langages orientés objet, List<>
n'est pas un type approprié en soi. Il n'a pas de méthodes et n'a donc pas d'interface. C'est seulement List<T>
cela qui a ces choses. Essentiellement, en termes plus techniques, les seuls types sur lesquels vous pouvez résumer de manière significative sont ceux avec le type *
. Afin d'utiliser des types de type supérieur dans un monde orienté objet, vous devez formuler des contraintes de type d'une manière cohérente avec cette restriction.
Par exemple, comme mentionné dans les commentaires, nous pouvons voir Option<>
et List<>
comme "mappable", en ce sens que si vous avez une fonction, vous pouvez convertir un Option<T>
en un Option<S>
, ou un List<T>
en un List<S>
. En se rappelant que les classes ne peuvent pas être utilisées pour abstraire directement des types de type supérieur, nous faisons plutôt une interface:
IMappable<K<_>, T> where K<T> : IMappable<K<_>, T>
Et puis nous implémentons l'interface à la fois List<T>
et en tant Option<T>
que IMappable<List<_>, T>
et IMappable<Option<_>, T>
respectivement. Ce que nous avons fait, c'est d'utiliser des types de type supérieur pour placer des contraintes sur les types réels (non de type supérieur) Option<T>
et List<T>
. C'est ainsi que cela se fait dans Scala, bien que Scala ait bien sûr des caractéristiques telles que des traits, des variables de type et des paramètres implicites qui le rendent plus expressif.
Dans d'autres langues, il est possible d'abstraire directement des types de type supérieur. Dans Haskell, l'une des plus hautes autorités sur les systèmes de types, nous pouvons formuler une classe de type pour n'importe quel type, même si elle a un type supérieur. Par exemple,
class Mappable mp where
map :: mp a -> mp b
Il s'agit d'une contrainte placée directement sur un type (non spécifié) mp
qui prend un paramètre de type et nécessite qu'il soit associé à la fonction map
qui transforme un mp<a>
en un mp<b>
. Nous pouvons alors écrire des fonctions qui contraignent des types de plus haut niveau, Mappable
tout comme dans les langages orientés objet, vous pouvez placer une contrainte d'héritage. Eh bien, en quelque sorte.
Pour résumer, votre capacité à utiliser des types de type supérieur dépend de votre capacité à les contraindre ou à les utiliser dans le cadre de contraintes de type.