L'effacement de type est bon
Tenons-nous en aux faits
Un grand nombre de réponses à ce jour concernent trop l'utilisateur Twitter. Il est utile de rester concentré sur les messages et non sur le messager. Il y a un message assez cohérent avec même juste les extraits mentionnés jusqu'à présent:
C'est drôle quand les utilisateurs de Java se plaignent de l'effacement de type, qui est la seule chose que Java a eu raison, tout en ignorant tout ce qui ne va pas.
J'obtiens d'énormes avantages (par exemple la paramétrie) et un coût nul (le coût présumé est une limite d'imagination).
new T est un programme cassé. Il est isomorphe à l'affirmation «toutes les propositions sont vraies». Je ne suis pas grand dedans.
Un objectif: des programmes raisonnables
Ces tweets reflètent une perspective qui ne s'intéresse pas à savoir si nous pouvons faire faire quelque chose à la machine , mais plutôt à savoir si nous pouvons penser que la machine fera quelque chose que nous voulons réellement. Un bon raisonnement est une preuve. Les preuves peuvent être spécifiées en notation formelle ou quelque chose de moins formel. Quel que soit le langage de spécification, ils doivent être clairs et rigoureux. Les spécifications informelles ne sont pas impossibles à structurer correctement, mais sont souvent défectueuses dans la programmation pratique. Nous nous retrouvons avec des remédiations comme des tests automatisés et exploratoires pour compenser les problèmes que nous avons avec le raisonnement informel. Cela ne veut pas dire que les tests sont intrinsèquement une mauvaise idée, mais l'utilisateur de Twitter cité suggère qu'il existe un bien meilleur moyen.
Notre objectif est donc d'avoir des programmes corrects sur lesquels nous pouvons raisonner clairement et rigoureusement d'une manière qui correspond à la manière dont la machine exécutera réellement le programme. Ce n’est cependant pas le seul objectif. Nous voulons également que notre logique ait un certain degré d'expressivité. Par exemple, il n'y a que tant de choses que nous pouvons exprimer avec la logique propositionnelle. C'est bien d'avoir une quantification universelle (∀) et existentielle (∃) à partir de quelque chose comme la logique du premier ordre.
Utiliser des systèmes de types pour le raisonnement
Ces objectifs peuvent être très bien traités par les systèmes de types. Cela est particulièrement clair en raison de la correspondance Curry-Howard . Cette correspondance est souvent exprimée par l'analogie suivante: les types sont aux programmes comme les théorèmes sont aux preuves.
Cette correspondance est assez profonde. Nous pouvons prendre des expressions logiques et les traduire par la correspondance aux types. Ensuite, si nous avons un programme avec le même type de signature qui compile, nous avons prouvé que l'expression logique est universellement vraie (une tautologie). C'est parce que la correspondance est bidirectionnelle. La transformation entre les mondes type / programme et théorème / preuve est mécanique et peut dans de nombreux cas être automatisée.
Curry-Howard joue bien dans ce que nous aimerions faire avec les spécifications d'un programme.
Les systèmes de types sont-ils utiles en Java?
Même avec une compréhension de Curry-Howard, certaines personnes trouvent qu'il est facile de rejeter la valeur d'un système de types, quand il
- il est extrêmement difficile de travailler avec
- correspond (par Curry-Howard) à une logique à expressivité limitée
- est cassé (ce qui permet de caractériser les systèmes comme «faibles» ou «forts»).
En ce qui concerne le premier point, peut-être que les IDE rendent le système de types de Java assez facile à utiliser (c'est très subjectif).
En ce qui concerne le deuxième point, Java correspond presque à une logique du premier ordre. Les génériques donnent à utiliser l'équivalent du système de type de la quantification universelle. Malheureusement, les jokers ne nous donnent qu'une petite fraction de la quantification existentielle. Mais la quantification universelle est un bon début. C'est bien de pouvoir dire que cela fonctionne de manière List<A>
universelle pour toutes les listes possibles car A est complètement libre. Cela conduit à ce dont l'utilisateur de Twitter parle en ce qui concerne la «paramétrie».
Un article souvent cité sur la paramétrie est les théorèmes de Philip Wadler gratuitement! . Ce qui est intéressant à propos de cet article, c'est qu'à partir de la seule signature de type, nous pouvons prouver quelques invariants très intéressants. Si nous devions écrire des tests automatisés pour ces invariants, nous perdrions beaucoup de temps. Par exemple, pour List<A>
, à partir de la signature de type uniquement pourflatten
<A> List<A> flatten(List<List<A>> nestedLists);
on peut raisonner que
flatten(nestedList.map(l -> l.map(any_function)))
≡ flatten(nestList).map(any_function)
C'est un exemple simple, et vous pouvez probablement raisonner à ce sujet de manière informelle, mais c'est encore plus agréable lorsque nous obtenons de telles preuves formellement gratuitement à partir du système de types et vérifiées par le compilateur.
Ne pas effacer peut conduire à des abus
Du point de vue de l'implémentation du langage, les génériques de Java (qui correspondent à des types universels) jouent très fortement dans la paramétrie utilisée pour obtenir des preuves de ce que font nos programmes. Cela arrive au troisième problème mentionné. Tous ces gains de preuve et d'exactitude nécessitent un système de type sonore implémenté sans défauts. Java a certainement des fonctionnalités de langage qui nous permettent de briser notre raisonnement. Ceux-ci incluent, mais ne sont pas limités à:
- effets secondaires avec un système externe
- réflexion
Les génériques non effacés sont à bien des égards liés à la réflexion. Sans effacement, des informations d'exécution sont fournies avec l'implémentation que nous pouvons utiliser pour concevoir nos algorithmes. Cela signifie que statiquement, lorsque nous raisonnons sur les programmes, nous n'avons pas une vue d'ensemble complète. La réflexion menace gravement l'exactitude de toutes les preuves sur lesquelles nous raisonnons statiquement. Ce n'est pas une coïncidence, la réflexion conduit également à une variété de défauts délicats.
Alors, quelles sont les façons dont les génériques non effacés pourraient être «utiles»? Considérons l'utilisation mentionnée dans le tweet:
<T> T broken { return new T(); }
Que se passe-t-il si T n'a pas de constructeur sans argument? Dans certaines langues, ce que vous obtenez est nul. Ou peut-être ignorez-vous la valeur nulle et passez directement à la levée d'une exception (à laquelle les valeurs nulles semblent conduire de toute façon). Parce que notre langage est Turing complet, il est impossible de raisonner pour savoir quels appels à broken
impliqueront des types "sûrs" avec des constructeurs sans argument et lesquels ne le seront pas. Nous avons perdu la certitude que notre programme fonctionne universellement.
Effacer signifie que nous avons raisonné (alors effaçons)
Donc, si nous voulons raisonner sur nos programmes, il nous est fortement déconseillé d'utiliser des fonctionnalités de langage qui menacent fortement notre raisonnement. Une fois que nous avons fait cela, alors pourquoi ne pas simplement supprimer les types au moment de l'exécution? Ils ne sont pas nécessaires. Nous pouvons obtenir une certaine efficacité et simplicité avec la satisfaction qu'aucune conversion n'échouera ou que des méthodes pourraient manquer lors de l'invocation.
L'effacement encourage le raisonnement.