Pourquoi avez-vous besoin de types plus élevés?


13

Certains langages permettent des classes et des fonctions avec des paramètres de type (comme List<T>Tpeut être un type arbitraire). Par exemple, vous pouvez avoir une fonction comme:

List<S> Function<S, T>(List<T> list)

Cependant, certaines langues permettent d'étendre ce concept d'un niveau supérieur, vous permettant d'avoir une fonction avec la signature:

K<S> Function<K<_>, S, T>(K<T> arg)

K<_>lui-même est un type comme List<_>celui qui a un paramètre de type. Ce "type partiel" est appelé constructeur de type.

Ma question est, pourquoi avez-vous besoin de cette capacité? Il est logique d'avoir un type comme List<T>parce que tous List<T>sont presque exactement les mêmes, mais tous K<_>peuvent être entièrement différents. Vous pouvez avoir un Option<_>et un List<_>qui n'ont aucune fonctionnalité commune.


7
Il y a plusieurs bonnes réponses ici: stackoverflow.com/questions/21170493/…
itsbruce

3
@itsbruce En particulier, l' Functorexemple de la réponse de Luis Casillas est assez intuitif. Qu'ont List<T>et Option<T>ont en commun? Si vous me donnez une et une fonction, T -> Sje peux vous donner un List<S>ou Option<S>. Une autre chose qu'ils ont en commun est que vous pouvez essayer d'obtenir une Tvaleur des deux.
Doval

@Doval: Comment feriez-vous le premier? Dans la mesure où l'on s'intéresse à ce dernier, je pense que cela pourrait être géré en faisant implémenter les deux types IReadableHolder<T>.
supercat

@supercat Je devine qu'ils devraient mettre en œuvre IMappable<K<_>, T>la méthode K<S> Map(Func<T, S> f), la mise en œuvre comme IMappable<Option<_>, T>, IMappable<List<_>, T>. Il faudrait donc contraindre K<T> : IMappable<K<_>, T>à en tirer le meilleur parti.
GregRos

1
Le casting n'est pas une analogie appropriée. Ce qui est fait avec les classes de types (ou constructions similaires), c'est que vous montrez comment un type donné satisfait le type de type supérieur. Cela implique généralement de définir de nouvelles fonctions ou de montrer quelles fonctions existantes (ou méthodes s'il s'agit d'un langage OO comme Scala) peuvent être utilisées. Une fois cela fait, toutes les fonctions définies pour fonctionner avec le type supérieur fonctionneront toutes avec ce type. Mais c'est bien plus qu'une interface car plus qu'un simple ensemble de signatures de fonction / méthode est défini. Je suppose que je vais devoir aller écrire une réponse pour montrer comment cela fonctionne.
itsbruce

Réponses:


5

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 Tvotre 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é) mpqui prend un paramètre de type et nécessite qu'il soit associé à la fonction mapqui transforme un mp<a>en un mp<b>. Nous pouvons alors écrire des fonctions qui contraignent des types de plus haut niveau, Mappabletout 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.


J'ai été trop occupé à changer d'emploi, mais je trouverai enfin le temps d'écrire ma propre réponse. Je pense cependant que vous avez été distrait par l'idée de contraintes. Un aspect important des types de type supérieur est qu'ils permettent de définir des fonctions / comportements qui peuvent être appliqués à n'importe quel type dont on peut logiquement démontrer la qualification. Loin d'imposer des contraintes aux types existants, les classes de types (une application de types de type supérieur) leur ajoutent un comportement.
itsbruce

Je pense que c'est une question de sémantique. Une classe de type définit une classe de types. Lorsque vous définissez une fonction telle que (Mappable mp) => mp a -> mp b, vous avez placé une contrainte mppour être membre de la classe de type Mappable. Lorsque vous déclarez un type tel qu'il Optionest une instance de Mappable, vous ajoutez un comportement à ce type. Je suppose que vous pouvez utiliser ce comportement localement sans jamais contraindre aucun type, mais ce n'est pas différent de définir une fonction ordinaire.
GregRos

En outre, je suis tout à fait certain que les classes de types ne sont pas directement liées aux types de type supérieur. Scala a des types de type supérieur, mais pas de classes de type, et vous pouvez restreindre les classes de type aux types avec le type *sans les rendre inutilisables. Il est certainement vrai que les classes de types sont très puissantes lorsque vous travaillez avec des types de type supérieur.
GregRos

Scala a des classes de type. Ils sont implémentés avec implicites. Les implicits fournissent l'équivalent des instances de classe de type Haskell. C'est une implémentation plus fragile que Haskell car il n'y a pas de syntaxe dédiée pour cela. Mais cela a toujours été l'un des objectifs clés des implicites Scala et ils font le travail.
itsbruce
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.