web-dev-qa-db-fra.com

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

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 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'un autre côté, 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.
29

Le système de types de Haskell est entièrement déductible (en laissant de côté la récursion polymorphe, certains langageextensions , et le redouté restriction du monomorphisme ), mais les programmeurs fournissent encore fréquemment du type annotations dans le code source même quand elles 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 général moins 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 Functor, pas seulement aux listes. Mais il a été estimé que map serait plus facile à comprendre pour les débutants, il vit donc 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 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: 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 globalement inférables.

44
Benjamin Hodgson

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, var est utilisé dans les situations où il est gênant 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 quoi 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 y a des situations où l'inférence de type ne peut pas être utilisée, comme les déclarations de variable membre dont les valeurs sont définies au moment de la construction, ou où vous voulez vraiment que l'intellisense fonctionne correctement (Hackerrank's IDE n'intellisense pas le membres de la variable, sauf si vous déclarez le type explicitement).

8
Robert Harvey

Bonne question!

  1. Étant donné que le type n'est pas explicitement annoté, il peut parfois rendre le code plus difficile à lire, ce qui entraîne davantage 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. Comme le type n'est pas explicitement annoté, votre IDE ne peut pas deviner aussi bien 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.

7
Telastyn

Cela me semble bien, mais je me demande s'il y a des compromis ou pourquoi disons 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 mot clé auto 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.

Donc, le véritable inconvénient est quand un langage ne le fait pas supporte le typage implicite, car avec cela, il déclare qu'il n'y a aucune raison légitime pour le développeur de l'utiliser, ce qui n'est pas vrai.

4
Gábor Angyal

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 peut 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 à contraintes: lorsque vous utilisez un type, vous contraignez 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, les valeurs polymorphes doivent être évaluées plusieurs fois pour chaque type auquel elles sont évaluées, motivant la restriction du monomorphisme.

1
Edward Z. Yang