Un bon système de type générique

29

Il est communément admis que les génériques Java ont échoué de plusieurs manières importantes. La combinaison de caractères génériques et de limites a conduit à un code très illisible.

Cependant, quand je regarde d'autres langages, je n'arrive vraiment pas à trouver un système de type générique qui plaise aux programmeurs.

Si nous prenons les éléments suivants comme objectifs de conception d'un tel système de types:

  • Produit toujours des déclarations de type faciles à lire
  • Facile à apprendre (pas besoin de réviser la covariance, la contravariance, etc.)
  • maximise le nombre d'erreurs de compilation

Y a-t-il une langue qui a bien fait les choses? Si je google, la seule chose que je vois est des plaintes sur la façon dont le système de type aspire dans la langue X. Ce type de complexité est-il inhérent à la frappe générique? Faut-il simplement renoncer à essayer de vérifier la sécurité du type à 100% au moment de la compilation?

Ma principale question est de savoir quel est le langage qui a "bien fait les choses" en ce qui concerne ces trois objectifs. Je me rends compte que c'est subjectif, mais jusqu'à présent, je ne peux même pas trouver une langue où tous les programmeurs ne conviennent pas que le système de type générique est un gâchis.

Addendum: comme indiqué, la combinaison de sous-typage / héritage et génériques est ce qui crée la complexité, donc je suis vraiment à la recherche d'un langage qui combine les deux et évite l'explosion de la complexité.

Peter
la source
2
Qu'entendez-vous par easy-to-read type declarations? Le troisième critère est également ambigu: par exemple, je peux transformer les exceptions d'index de tableau hors limites en erreurs de temps de compilation en ne vous laissant pas indexer les tableaux sauf si je peux calculer l'index au moment de la compilation. De plus, le deuxième critère exclut le sous-typage. Ce n'est pas nécessairement une mauvaise chose, mais vous devez être conscient de ce que vous demandez.
Doval
17
Voir la plupart des langages
AlexFoxGill
9
@gnat, ce n'est certainement pas une diatribe contre Java. Je programme presque exclusivement en Java. Mon point est qu'il est généralement admis au sein de la communauté Java que les génériques sont défectueux (pas un échec total, mais probablement partiel), il est donc logique de se demander comment ils auraient dû être mis en œuvre. Pourquoi ont-ils tort et les autres ont-ils bien compris? Ou est-il réellement impossible d'obtenir des génériques absolument exacts?
Peter
1
Si tout le monde venait de voler de C #, il y aurait moins de plaintes. Surtout Java est en mesure de rattraper son retard en copiant. Au lieu de cela, ils décident de solutions inférieures. De nombreuses questions que les comités de conception Java discutent encore ont déjà été décidées et implémentées en C #. Ils ne semblent même pas regarder.
usr
2
@emodendroket: Je pense que mes deux plus grandes plaintes au sujet des génériques C # sont qu'il n'y a aucun moyen d'appliquer une contrainte de "supertype" (par exemple Foo<T> where SiameseCat:T) et qu'il n'y a aucune possibilité d'avoir un type générique qui ne soit pas convertible en Object. À mon humble avis, .NET bénéficierait de types d'agrégats similaires aux structures, mais encore plus désossés. S'il KeyValuePair<TKey,TValue>s'agissait d'un tel type, alors un IEnumerable<KeyValuePair<SiameseCat,FordFocus>>pourrait être converti en IEnumerable<KeyValuePair<Animal,Vehicle>>, mais uniquement si le type ne pouvait pas être encadré.
supercat

Réponses:

24

Bien que les génériques soient courants dans la communauté de la programmation fonctionnelle depuis des décennies, l'ajout de génériques aux langages de programmation orientés objet offre certains défis uniques, en particulier l'interaction du sous-typage et des génériques.

Cependant, même si nous nous concentrons sur les langages de programmation orientés objet, et Java en particulier, un système générique bien meilleur aurait pu être conçu:

  1. Les types génériques devraient être admissibles partout où se trouvent d'autres types. En particulier, si Test un paramètre de type, les expressions suivantes doivent être compilées sans avertissement:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Oui, cela nécessite la réification des génériques, comme tous les autres types de la langue.

  2. La covariance et la contravariance d'un type générique doivent être spécifiées dans (ou déduites de) sa déclaration, plutôt que chaque fois que le type générique est utilisé, afin que nous puissions écrire

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    plutôt que

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Comme les types génériques peuvent devenir assez longs, nous ne devrions pas avoir besoin de les spécifier de manière redondante. Autrement dit, nous devrions être en mesure d'écrire

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    plutôt que

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Tout type doit être admissible en tant que paramètre de type, pas seulement les types de référence. (Si nous pouvons en avoir un int[], pourquoi ne pouvons-nous pas en avoir un List<int>)?

Tout cela est possible en C #.

meriton - en grève
la source
1
Cela éliminerait-il également les génériques autoréférentiels? Et si je veux dire qu'un objet comparable peut se comparer à n'importe quoi du même type ou d'une sous-classe? Cela peut-il être fait? Ou si j'écris une méthode de tri qui accepte des listes avec des objets comparables, elles doivent toutes être comparables les unes aux autres. Enum est un autre bon exemple: Enum <E étend Enum <E>>. Je ne dis pas qu'un système de type devrait être capable de faire cela, je suis juste curieux de savoir comment C # gère ces situations.
Peter
1
L' inférence de type générique de Java 7 et l' aide automatique de C ++ à certaines de ces préoccupations, mais sont du sucre syntaxique et ne changent pas les mécanismes sous-jacents.
L'inférence de type de @Snowman Java a cependant des cas de coin vraiment désagréables, comme ne pas travailler du tout avec des classes anonymes et ne pas trouver les bonnes limites pour les caractères génériques lorsque vous évaluez une méthode générique comme argument d'une autre méthode générique.
Doval
@Doval, c'est pourquoi j'ai dit que cela répond à certaines des préoccupations: il ne règle rien et ne résout pas tout. Les génériques Java ont beaucoup de problèmes: bien que meilleurs que les types bruts, ils causent certainement de nombreux maux de tête.
34

L'utilisation de sous-types crée de nombreuses complications lors de la programmation générique. Si vous insistez pour utiliser un langage avec des sous-types, vous devez accepter qu'il y a une certaine complexité inhérente à la programmation générique qui l'accompagne. Certaines langues le font mieux que d'autres, mais vous ne pouvez pas aller plus loin.

Comparez cela aux génériques de Haskell, par exemple. Ils sont suffisamment simples pour que si vous utilisez l'inférence de type, vous pouvez écrire une fonction générique correcte par accident . En fait, si vous spécifiez un seul type, le compilateur dit souvent à lui - même: « Eh bien, j'été va faire ce générique, mais vous m'a demandé de le faire que pour ints, donc peu importe. »

Certes, les gens utilisent le système de type de Haskell de manière étonnamment complexe, ce qui en fait le fléau de chaque débutant, mais le système de type sous-jacent lui-même est élégant et très admiré.

Karl Bielefeldt
la source
1
Merci pour cette réponse. Cet article commence par quelques exemples de Joshua Bloch où les génériques deviennent trop compliqués: artima.com/weblogs/viewpost.jsp?thread=222021 . Est-ce une différence de culture entre Java et Haskell, où de telles constructions seraient considérées comme correctes dans Haskell, ou y a-t-il une réelle différence avec le système de type de Haskell qui évite de telles situations?
Peter
10
@Peter Haskell n'a pas de sous-typage, et comme Karl l'a dit, le compilateur peut déduire automatiquement les types en incluant des contraintes comme "le type adoit être une sorte d'entier".
Doval
En d'autres termes, la covariance , dans des langues comme Scala.
Paul Draper
14

Il y a eu pas mal de recherches sur la combinaison des génériques avec le sous-typage il y a environ 20 ans. Le langage de programmation Thor développé par le groupe de recherche de Barbara Liskov au MIT avait une notion de clauses «où» qui vous permettent de spécifier les exigences du type sur lequel vous paramétrez. (Ceci est similaire à ce que C ++ essaie de faire avec les concepts .)

L'article décrivant les génériques de Thor et comment ils interagissent avec les sous-types de Thor est: Day, M; Gruber, R; Liskov, B; Myers, AC: Subtypes vs. where clauses: constraining parametric polymorphism , ACM Conf on Obj-Oriented Prog, Sys, Lang and Apps , (OOPSLA-10): 156-158, 1995.

Je crois qu'ils ont, à leur tour, construit sur le travail qui a été fait sur Emerald à la fin des années 1980. (Je n'ai pas lu ce travail, mais la référence est: Black, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Distribution and Abstract Types in Emerald , _IEEE T. Software Eng., 13 ( 1): 65-76, 1987.

Thor et Emerald étaient tous les deux des "langues académiques", donc ils n'ont probablement pas été suffisamment utilisés pour que les gens comprennent vraiment si les clauses (concepts) résolvent vraiment de réels problèmes. Il est intéressant de lire l'article de Bjarne Stroustrup sur les raisons de l'échec du premier essai de Concepts in C ++: Stroustrup, B: The C ++ 0x "Remove Concepts" Decision , Dr Dobbs , 22 juillet 2009. (Plus d'informations sur la page d'accueil de Stroustrup . )

Une autre direction que les gens semblent essayer est quelque chose appelé traits . Par exemple, le langage de programmation Rust de Mozilla utilise des traits. Si je comprends bien (ce qui peut être complètement faux), déclarer qu'une classe satisfait un trait est très similaire à dire qu'une classe implémente une interface, mais vous dites "se comporte comme un" plutôt que "est un". Il semble que les nouveaux langages de programmation Swift d'Apple utilisent un concept similaire de protocoles pour spécifier les contraintes sur les paramètres des génériques .

Logique errante
la source
Les traits de rouille sont similaires aux classes de type Haskell. Mais il y a deux angles, on peut le regarder: 1. Le voir comme un moyen d'énoncer des contraintes. 2. Définissez une interface générique, qui peut être associée à un type concret ou à une classe générique de types.
BitTickler il y a