Quel est le compromis pour l'inférence de type?

29

Il semble que tous les nouveaux langages de programmation ou du moins ceux qui sont devenus populaires utilisent l'inférence de type. Même Javascript a obtenu des types et des inférences de types via diverses implémentations (Acscript, typescript, etc.). Cela me semble bien, mais je me demande s'il y a des compromis ou pourquoi disons que Java ou les anciens bons langages n'ont pas d'inférence de type

  • Lorsque vous déclarez une variable dans Go sans spécifier son type (en utilisant var sans type ou la syntaxe: =), le type de la variable est déduit de la valeur sur le côté droit.
  • D permet d'écrire de gros fragments de code sans spécifier de types redondants, comme le font les langages dynamiques. D'autre part, l'inférence statique déduit des types et d'autres propriétés de code, donnant le meilleur des mondes statique et dynamique.
  • Le moteur d'inférence de type dans Rust est assez intelligent. Il fait plus que regarder le type de la valeur r lors d'une initialisation. Il examine également comment la variable est utilisée par la suite pour déduire son type.
  • Swift utilise l'inférence de type pour déterminer le type approprié. L'inférence de type permet à un compilateur de déduire automatiquement le type d'une expression particulière lorsqu'il compile votre code, simplement en examinant les valeurs que vous fournissez.
L'utilisateur sans chapeau
la source
3
En C #, les directives générales disent de ne pas toujours utiliser varcar cela peut parfois nuire à la lisibilité.
Mephy
5
"... ou pourquoi disons que Java ou les anciens bons langages n'ont pas d'inférence de type" Les raisons sont probablement historiques; ML est apparu 1 an après C selon Wikipedia et il y avait une inférence de type. Java essayait de plaire aux développeurs C ++ . C ++ a commencé comme une extension de C, et les principales préoccupations de C étaient d'être un wrapper portable sur l'assemblage et d'être facile à compiler. Pour tout ce que ça vaut, j'ai lu que le sous-typage rend l'inférence de type indécidable dans le cas général également.
Doval
3
@Doval Scala semble faire un assez bon travail pour déduire les types, pour un langage qui prend en charge l'héritage des sous-types. Ce n'est pas aussi bon que n'importe quel langage de la famille ML, mais c'est probablement aussi bon que vous pourriez le demander, étant donné la conception du langage.
KChaloux
12
Il vaut la peine de faire une distinction entre la déduction de type (monodirectionnelle, comme C # varet C ++ auto) et l' inférence de type (bidirectionnelle, comme Haskell let). Dans le premier cas, le type d'un nom peut être déduit uniquement de son initialiseur - ses utilisations doivent suivre le type du nom. Dans ce dernier cas, le type d'un nom peut également être déduit de ses utilisations, ce qui est utile en ce que vous pouvez écrire simplement []pour une séquence vide, quel que soit le type d'élément, ou newEmptyMVarpour une nouvelle référence mutable nulle, quel que soit le référent type.
Jon Purdy
L'inférence de type peut être incroyablement compliquée à mettre en œuvre, particulièrement efficacement. Les gens ne veulent pas que leur complexité de compilation souffre d'une explosion exponentielle d'expressions plus complexes. Lecture intéressante: cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
Alexander -

Réponses:

43

Le système de types de Haskell est entièrement déductible (en laissant de côté la récursion polymorphe, certaines extensions de langage et la restriction redoutée du monomorphisme ), mais les programmeurs fournissent encore fréquemment des annotations de type dans le code source même lorsqu'ils n'en ont pas besoin. Pourquoi?

  1. Les annotations de type servent de documentation . Cela est particulièrement vrai avec des types aussi expressifs que ceux de Haskell. Étant donné le nom et le type d'une fonction, vous pouvez généralement avoir une assez bonne idée de ce que fait la fonction, en particulier lorsque la fonction est polymorphe paramétrique.
  2. Les annotations de type peuvent stimuler le développement . Écrire une signature de type avant d'écrire le corps d'une fonction ressemble à un développement piloté par les tests. D'après mon expérience, une fois que vous avez compilé une fonction Haskell, elle fonctionne généralement du premier coup. (Bien sûr, cela n'élimine pas la nécessité de tests automatisés!)
  3. Les types explicites peuvent vous aider à vérifier vos hypothèses . Lorsque j'essaie de comprendre du code qui fonctionne déjà, je le parsème fréquemment avec ce que je pense être les annotations de type correct. Si le code compile toujours, je sais que je l'ai compris. Si ce n'est pas le cas, j'ai lu le message d'erreur.
  4. Les signatures de type vous permettent de spécialiser les fonctions polymorphes . Très occasionnellement, une API est plus expressive ou utile si certaines fonctions ne sont pas polymorphes. Le compilateur ne se plaindra pas si vous donnez à une fonction un type moins général que ce qui serait déduit. L'exemple classique est map :: (a -> b) -> [a] -> [b]. Sa forme plus générale ( fmap :: Functor f => (a -> b) -> f a -> f b) s'applique à tous les Functors, pas seulement aux listes. Mais il a été estimé que ce mapserait plus facile à comprendre pour les débutants, alors il vit aux côtés de son grand frère.

Dans l'ensemble, les inconvénients d'un système statiquement typé mais déductible sont à peu près les mêmes que les inconvénients de la frappe statique en général, une discussion bien rodée sur ce site et bien d'autres de pages de guerres de flammes). Bien sûr, certains desdits inconvénients sont améliorés par la plus petite quantité d'annotations de type dans un système inférable. De plus, l'inférence de type a ses propres avantages: le développement par trous ne serait pas possible sans l'inférence de type.

Java * prouve qu'un langage nécessitant trop d'annotations de type devient ennuyeux, mais avec trop peu, vous perdez les avantages que j'ai décrits ci-dessus. Les langues avec inférence de type opt-out établissent un équilibre agréable entre les deux extrêmes.

* Même Java, ce grand bouc émissaire, effectue une certaine quantité d' inférence de type local . Dans une instruction comme Map<String, Integer> = new HashMap<>();, vous n'avez pas besoin de spécifier le type générique du constructeur. D'un autre côté, les langages de style ML sont généralement inférables à l' échelle mondiale.

Benjamin Hodgson
la source
2
Mais vous n'êtes pas un débutant;) Vous devez avoir une compréhension des classes de types et des types avant de vraiment "obtenir" Functor. Les listes et mapsont plus susceptibles d'être familières aux Haskellers non chevronnés.
Benjamin Hodgson
1
Considérez-vous les C # varcomme un exemple d'inférence de type?
Benjamin Hodgson
1
Oui, C # varest un bon exemple.
Doval
1
Le site signale déjà cette discussion comme trop longue donc ce sera ma dernière réponse. Dans le premier cas, le compilateur doit déterminer le type de x; dans ce dernier, il n'y a pas de type à déterminer, tous les types sont connus et il suffit de vérifier que l'expression a du sens. La différence devient plus importante lorsque vous passez devant des exemples triviaux et xest utilisée à plusieurs endroits; le compilateur doit alors recouper les emplacements où xest utilisé pour déterminer 1) est-il possible d'affecter xun type tel que le code saisira check? 2) Si c'est le cas, quel est le type le plus général que nous pouvons lui attribuer?
Doval
1
Notez que la new HashMap<>();syntaxe n'a été ajoutée qu'en Java 7, et les lambdas en Java 8 permettent beaucoup d'inférence de type "réel".
Michael Borgwardt
8

En C #, l'inférence de type se produit au moment de la compilation, donc le coût d'exécution est nul.

Pour des raisons de style, varest utilisé dans les situations où il n'est pas pratique ou inutile de spécifier manuellement le type. Linq est une de ces situations. Un autre est:

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

sans lequel vous répéteriez votre nom de type très long (et les paramètres de type) plutôt que de simplement dire var.

Utiliser le nom réel du type lorsqu'il est explicite améliore la clarté du code.

Il existe des situations où l'inférence de type ne peut pas être utilisée, comme les déclarations de variables de membre dont les valeurs sont définies au moment de la construction, ou où vous voulez vraiment qu'intellisense fonctionne correctement (l'EDI de Hackerrank n'intellectera pas les membres de la variable à moins que vous ne déclariez le type explicitement) .

Robert Harvey
la source
16
"En C #, l'inférence de type se produit au moment de la compilation, donc le coût d'exécution est nul." L'inférence de type se produit au moment de la compilation par définition, c'est donc le cas dans toutes les langues.
Doval
Tant mieux.
Robert Harvey
1
@Doval, il est possible d'effectuer l'inférence de type lors de la compilation JIT, ce qui a clairement un coût d'exécution.
Jules
6
@Jules Si vous êtes parvenu à exécuter le programme, il a déjà été vérifié par type; il n'y a plus rien à déduire. Ce dont vous parlez ne s'appelle généralement pas l'inférence de type, bien que je ne sois pas sûr du terme approprié.
Doval
7

Bonne question!

  1. Étant donné que le type n'est pas explicitement annoté, il peut parfois rendre le code plus difficile à lire - conduisant à plus de bogues. Bien utilisé, il rend bien sûr le code plus propre et plus lisible. Si vous êtes pessimiste et pensez que la plupart des programmeurs sont mauvais (ou travaillent là où la plupart des programmeurs sont mauvais), ce sera une perte nette.
  2. Bien que l'algorithme d'inférence de type soit relativement simple, il n'est pas gratuit . Ce genre de chose augmente légèrement le temps de compilation.
  3. Étant donné que le type n'est pas explicitement annoté, votre IDE ne peut pas aussi deviner ce que vous essayez de faire, ce qui nuit à la saisie semi-automatique et aux assistants similaires pendant le processus de déclaration.
  4. Combiné avec des surcharges de fonctions, vous pouvez parfois vous retrouver dans des situations où l'algorithme d'inférence de type ne peut pas décider quel chemin prendre, ce qui conduit à des annotations de style de casting plus moches. (Cela se produit un peu avec la syntaxe de fonction anonyme de C # par exemple).

Et il y a des langages plus ésotériques qui ne peuvent pas faire leur bizarrerie sans annotation de type explicite. Jusqu'à présent, il n'y en a aucun que je sache qui soit assez commun / populaire / prometteur pour le mentionner, sauf en passant.

Telastyn
la source
1
la saisie semi-automatique ne se soucie pas de l'inférence de type. Peu importe comment le type a été décidé, seulement ce que le type a. Et les problèmes avec les lambdas de C # n'ont rien à voir avec l'inférence, c'est parce que les lambdas sont polymorphes mais le système de type ne peut pas y faire face - ce qui n'a rien à voir avec l'inférence.
DeadMG
2
@deadmg - bien sûr, une fois que la variable est déclarée, elle n'a pas de problème, mais pendant que vous tapez la déclaration de variable, elle ne peut pas restreindre les options du côté droit au type déclaré car il n'y a pas de type déclaré.
Telastyn
@deadmg - en ce qui concerne les lambdas, je ne suis pas très clair sur ce que vous voulez dire, mais je suis sûr que ce n'est pas entièrement correct en fonction de ma compréhension du fonctionnement des choses. Même quelque chose d'aussi simple que d' var foo = x => x;échec parce que la langue doit inférer xici et n'a rien à continuer. Lorsque les lambdas sont construits, ils sont construits en tant que délégués explicitement typés Func<int, int> foo = x => xest intégré dans CIL comme Func<int, int> foo = new Func<int, int>(x=>x);où le lambda est construit en tant que fonction générée et explicitement typée. Pour moi, inférer le type de xfait partie intégrante de l'inférence de type ...
Telastyn
3
@Telastyn Ce n'est pas vrai que la langue n'a plus rien à faire. Toutes les informations nécessaires pour taper l'expression x => xsont contenues dans le lambda lui-même - il ne fait référence à aucune variable de la portée environnante. Toute langue fonctionnelle serait sain d' esprit bien que son type infère est « pour tous les types a, a -> a». Je pense que DeadMG essaie de dire que le système de type de C # n'a pas la propriété de type principal, ce qui signifie qu'il est toujours possible de trouver le type le plus général pour n'importe quelle expression. C'est une propriété très facile à détruire.
Doval
@Doval - enh ... les objets fonction génériques n'existent pas en C #, ce qui, je pense, est subtilement différent de ce dont vous parlez tous les deux, même si cela conduit au même problème.
Telastyn
4

Cela me semble bien, mais je me demande s'il y a des compromis ou pourquoi disons que Java ou les anciens bons langages n'ont pas d'inférence de type

Java se trouve être l'exception plutôt que la règle ici. Même C ++ (que je crois qualifier de "bon vieux langage" :)) prend en charge l'inférence de type avec le automot - clé depuis la norme C ++ 11. Il fonctionne non seulement avec la déclaration de variable, mais aussi comme type de retour de fonction, ce qui est particulièrement pratique avec certaines fonctions de modèle complexes.

Le typage implicite et l'inférence de type ont de nombreux bons cas d'utilisation, et il y a aussi certains cas d'utilisation où vous ne devriez vraiment pas le faire. C'est parfois une question de goût et un sujet de débat.

Mais qu'il existe sans aucun doute de bons cas d'utilisation, est en soi une raison légitime pour tout langage de le mettre en œuvre. Ce n'est pas non plus une fonctionnalité difficile à implémenter, aucune pénalité d'exécution et n'affecte pas le temps de compilation de manière significative.

Je ne vois pas de réel inconvénient à donner au développeur la possibilité d'utiliser l'inférence de type.

Certains répondeurs se demandent à quel point la frappe explicite est bonne parfois, et ils ont certainement raison. Mais ne pas prendre en charge le typage implicite signifierait qu'un langage applique un typage explicite tout le temps.

Ainsi, le véritable inconvénient est lorsqu'un langage ne prend pas en charge le typage implicite, car avec cela, il indique qu'il n'y a aucune raison légitime pour le développeur de l'utiliser, ce qui n'est pas vrai.

Gábor Angyal
la source
1

La principale distinction entre un système d'inférence de type Hindley-Milner et l'inférence de type Go est la direction du flux d'informations. Dans HM, saisissez les flux d'informations vers l'avant et vers l'arrière via l'unification; dans Go, saisissez uniquement les flux d'informations vers l'avant: il calcule uniquement les substitutions vers l'avant.

L'inférence de type HM est une splendide innovation qui fonctionne bien avec les langages fonctionnels de type polymorphe, mais les auteurs de Go diraient probablement qu'elle essaie d'en faire trop:

  1. Le fait que l'information circule à la fois vers l'avant et vers l'arrière signifie que l'inférence de type HM est une affaire très non locale; en l'absence d'annotations de type, chaque ligne de code de votre programme pourrait contribuer à la saisie d'une seule ligne de code. Si vous ne disposez que d'une substitution directe, vous savez que la source d'une erreur de type doit être dans le code qui précède votre erreur; pas le code qui vient après.

  2. Avec l'inférence de type HM, vous avez tendance à penser aux contraintes: lorsque vous utilisez un type, vous limitez les types possibles qu'il peut avoir. À la fin de la journée, il peut y avoir certaines variables de type qui sont laissées totalement sans contrainte. La perspicacité de l'inférence de type HM est que, ces types n'ont vraiment pas d'importance, et donc ils sont transformés en variables polymorphes. Cependant, ce polymorphisme supplémentaire peut être indésirable pour un certain nombre de raisons. Tout d'abord, comme certaines personnes l'ont souligné, ce polymorphisme supplémentaire pourrait être indésirable: HM concluant un faux, de type polymorphe pour certains faux codes, et conduisant à d'étranges erreurs plus tard. Deuxièmement, lorsqu'un type est laissé polymorphe, cela peut avoir des conséquences sur le comportement d'exécution. Par exemple, un résultat intermédiaire trop polymorphe est la raison pour laquelle «montrer. lire 'est considéré comme ambigu à Haskell; comme autre exemple,

Edward Z. Yang
la source
2
Malheureusement, cela ressemble à une bonne réponse à une autre question. OP pose des questions sur "l'inférence de type Go-style" par rapport aux langages qui n'ont aucune inférence de type du tout, pas "Go-style" contre HM.
Ixrec
-2

Cela nuit à la lisibilité.

Comparer

var stuff = someComplexFunction()

contre

List<BusinessObject> stuff = someComplexFunction()

C'est particulièrement un problème lors de la lecture en dehors d'un IDE comme sur github.

miguel
la source
4
cela ne semble pas offrir quoi que ce soit de substantiel par rapport aux points soulevés et expliqués dans les 6 réponses précédentes
gnat
Si vous utilisez un nom propre au lieu de "complexe illisible", vous varpouvez l'utiliser sans nuire à la lisibilité. var objects = GetBusinessObjects();
Fabio
D'accord, mais vous divulguez le système de types dans les noms de variables. C'est comme la notation hongroise. Après les refactorisations, vous êtes plus susceptible de vous retrouver avec des noms qui ne correspondent pas au code. En général, le style fonctionnel vous pousse à perdre les avantages d'espacement de noms naturels fournis par les classes. Vous vous retrouvez donc avec plus squareArea () au lieu de square.area ().
miguel