Système de types
std :: any :: Any ' static Vérification statique et dynamique des types en pratique Le choix entre typage statique et typage dynamique nécessite certains compromis . Le typage s...
'staticVérification statique et dynamique des types en pratique
Le choix entre typage statique et typage dynamique nécessite certains compromis .
Le typage statique permet de détecter les erreurs de type de manière fiable à la compilation, ce qui accroît la fiabilité du programme livré. Cependant, les programmeurs divergent quant à la fréquence d'apparition des erreurs de type, ce qui engendre des désaccords supplémentaires sur la proportion de bogues qui seraient détectés par une représentation adéquate des types conçus dans le code. Les partisans du typage statique estiment que les programmes sont plus fiables lorsqu'ils ont fait l'objet d'une vérification de type rigoureuse, tandis que les partisans du typage dynamique mettent en avant le code distribué, dont la fiabilité est avérée, et la taille réduite des bases de données de bogues. L'intérêt du typage statique augmente avec la robustesse du système de types. Les partisans du typage dépendant , implémenté dans des langages tels que Dependent ML et Epigram , suggèrent que la quasi-totalité des bogues peuvent être considérés comme des erreurs de type, si les types utilisés dans un programme sont correctement déclarés par le programmeur ou correctement inférés par le compilateur.
Le typage statique permet généralement d'obtenir un code compilé plus rapide à l'exécution. Lorsque le compilateur connaît les types de données exacts utilisés (ce qui est nécessaire pour la vérification statique, par déclaration ou inférence), il peut générer un code machine optimisé. C'est pourquoi certains langages à typage dynamique, comme Common Lisp, autorisent les déclarations de types optionnelles à des fins d'optimisation.
À l'inverse, le typage dynamique peut permettre aux compilateurs de s'exécuter plus rapidement et aux interpréteurs de charger dynamiquement du nouveau code, car les modifications apportées au code source dans les langages à typage dynamique peuvent entraîner moins de vérifications à effectuer et moins de code à réexaminer.
Les langages statiquement typés dépourvus d'inférence de types (comme C et Java avant la version 10 ) exigent que les programmeurs déclarent les types utilisés par une méthode ou une fonction. Ceci constitue une documentation supplémentaire, active et dynamique, contrairement à la documentation statique. Le compilateur peut ainsi éviter toute désynchronisation et empêcher que cette documentation ne soit ignorée par les programmeurs. Cependant, un langage peut être statiquement typé sans exiger de déclarations de types (par exemple, Haskell , Scala , OCaml , F# , Swift et, dans une moindre mesure , C# et C++ ). Par conséquent, la déclaration explicite des types n'est pas requise pour le typage statique dans tous les langages.
Le typage dynamique autorise des constructions que certains typages statiques (simples) rejetteraient comme illégales. Par exemple, les fonctions `eval` , qui exécutent des données arbitraires comme du code, deviennent possibles. Une fonction `eval` est possible avec le typage statique, mais nécessite une utilisation avancée des types de données algébriques . De plus, le typage dynamique s'adapte mieux au code transitoire et au prototypage, permettant par exemple d'utiliser de manière transparente une structure de données factice ( objet factice ) à la place d'une structure de données complète (généralement à des fins d'expérimentation et de test).
Le typage dynamique permet généralement le typage canard (qui facilite la réutilisation du code ). De nombreux le typage canard ou d'autres mécanismes comme la programmation générique, qui facilitent également la réutilisation du code.
Le typage dynamique facilite généralement l'utilisation de la métaprogrammation . Par exemple, les modèles C++ sont généralement plus complexes à écrire que le code équivalent en Ruby ou Python , car le C++ impose des règles plus strictes concernant les définitions de types (pour les fonctions comme pour les variables). Cela oblige un développeur à écrire davantage de code répétitif pour un modèle qu'un développeur Python. Les constructions d'exécution plus avancées, telles que les métaclasses et l'introspection, sont souvent plus difficiles à utiliser dans les langages statiquement typés. Dans certains langages, ces fonctionnalités peuvent également servir, par exemple, à générer dynamiquement de nouveaux types et comportements à partir des données d'exécution. Ces constructions avancées sont souvent fournies par les langages de programmation dynamique ; nombre d'entre eux sont à typage dynamique, bien que le typage dynamique ne soit pas nécessairement lié aux langages de programmation dynamique .
systèmes de types forts et faibles
Une troisième façon de catégoriser le système de types d'un langage de programmation repose sur la sécurité des opérations et conversions typées. Les informaticiens utilisent le terme « langage à typage statique » pour décrire les langages qui n'autorisent pas les opérations ou conversions violant les règles du système de types.
Les informaticiens utilisent le terme « langage sûr en mémoire » (ou simplement « langage sûr ») pour décrire les langages qui interdisent aux programmes d'accéder à des zones de mémoire non allouées. Par exemple, un langage sûr en mémoire vérifie les limites des tableaux , ou garantit statiquement (c'est-à-dire à la compilation, avant l'exécution) que tout accès à des éléments hors des limites du tableau provoque des erreurs de compilation et éventuellement d'exécution.
Considérons le programme suivant d'un langage à la fois sûr en termes de types et sûr en termes de mémoire :
var x := 5; var y := "37"; var z := x + y;
Dans cet exemple, la variable Hello, World »), le résultat serait également conforme aux attentes. Un programme peut être sûr du point de vue des types et de la gestion de la mémoire et pourtant planter lors d'une opération invalide. Cela concerne les langages dont le système de types n'est pas suffisamment avancé pour spécifier précisément la validité des opérations sur tous les opérandes possibles. Mais si un programme rencontre une opération non sûre du point de vue des types, l'arrêt du programme est souvent la seule solution.
Considérons maintenant un exemple similaire en C :
int x = 5 ; char y [] = "37" ; char * z = x + y ; printf ( "%c " , * z );
Dans cet exemple, ` za` pointe vers une adresse mémoire située cinq caractères après `a` y, soit trois caractères après le caractère nul de fin de la chaîne pointée par `a` y. Il s'agit d'une zone mémoire à laquelle le programme n'est pas censé accéder. En C, ce comportement est indéfini et le programme peut effectuer n'importe quelle action ; avec un compilateur simple, il pourrait même afficher l'octet stocké après la chaîne « 37 ». Comme le montre cet exemple, le C n'est pas sûr en matière de gestion de la mémoire. De plus, comme des données arbitraires ont été considérées comme des caractères, le C n'est pas non plus un langage à typage statique.
En général, la sécurité des types et la sécurité de la mémoire sont indissociables. Par exemple, un langage qui prend en charge l'arithmétique des pointeurs et les conversions de nombres en pointeurs (comme le C) n'est ni sûr en termes de mémoire ni en termes de types, car il permet d'accéder à une zone mémoire arbitraire comme s'il s'agissait d'une zone mémoire valide de n'importe quel type.
Niveaux variables de vérification de type
Certains langages permettent d'appliquer différents niveaux de vérification à différentes régions du code. En voici quelques exemples :
- La
use strictdirective en JavaScript et Perl applique une vérification plus forte. - Le
declare(strict_types=1)en PHP sur une base de fichier permet uniquement une variable du type exact de la déclaration de type sera acceptée, ou uneTypeErrorexception sera levée. - En VB.NET,
Option Strict Oncela permet au compilateur d'exiger une conversion entre les objets.
Des outils supplémentaires tels que lint et IBM Rational Purify peuvent également être utilisés pour atteindre un niveau de rigueur plus élevé.
systèmes de type optionnel
Il a été proposé, principalement par Gilad Bracha , que le choix du système de types soit indépendant du choix du langage ; qu’un système de types soit un module pouvant être intégré au langage selon les besoins. Il estime que cela présente un avantage, car ce qu’il appelle les systèmes de types obligatoires rendent les langages moins expressifs et le code plus fragile. L’exigence selon laquelle le système de types n’affecte pas la sémantique du langage est difficile à satisfaire.
Le typage optionnel est lié au typage progressif , mais s'en distingue . Bien que les deux disciplines de typage puissent être utilisées pour effectuer une analyse statique du code ( typage statique ), les systèmes de types optionnels n'imposent pas la sécurité des types à l'exécution ( typage dynamique ).
Polymorphisme et types
float →float » désigne les fonctions prenant un argument flottant et renvoyant un flottant, alors l'intersection de ces deux types permet de décrire des fonctions qui effectuent l'une ou l'autre opération, selon le type d'entrée qu'elles reçoivent. Une telle fonction peut être passée sans problème à une autre fonction attendant une fonction «int →int » ; elle n'utilisera simplement pas la fonctionnalité «float → ».floatDans une hiérarchie de sous-classes, l'intersection d'un type et d'un type ancêtre (tel que son parent) est le type le plus dérivé. L'intersection des types frères est vide.
Le langage Forsythe inclut une implémentation générale des types d'intersection. Une forme restreinte est celle des types de raffinement .
Types d'union
Les types existentiels sont fréquemment utilisés avec les types d'enregistrements pour représenter des modules et des types de données abstraits , car ils permettent de dissocier l'implémentation de l'interface. Par exemple, le type « T = ∃X { a: X; f: (X → int); } » décrit une interface de module possédant un membre de données nommé a de type X et une fonction nommée f qui prend un paramètre du même type X et renvoie un entier. Ceci peut être implémenté de différentes manières ; par exemple :
- intT = { une : int ; f : (int → int) ; }
- floatT = { a: float; f: (float → int); }
Ces types sont tous deux des sous-types du type existentiel plus général T et correspondent à des types d'implémentation concrets ; par conséquent, toute valeur de l'un de ces types est une valeur de type T. Étant donné une valeur « t » de type « T », nous savons que « tf(ta) » est bien typé, quel que soit le type abstrait X. Ceci offre une grande flexibilité dans le choix des types adaptés à une implémentation particulière, tandis que les clients qui utilisent uniquement des valeurs du type d'interface ( le type existentiel ) sont protégés de ces choix.
En général, il est impossible pour le vérificateur de types de déduire le type existentiel auquel appartient un module donné. Dans l'exemple ci-dessus, `intT { a: int; f: (int → int); }` pourrait également être de type `∃X { a: X; f: (X → int); }`. La solution la plus simple consiste à annoter chaque module avec son type attendu, par exemple :
- intT = { une : int ; f : (int → int) ; } comme ∃X { une : X ; f : (X → int) ; }
Bien que les types de données abstraits et les modules aient été implémentés dans les langages de programmation depuis un certain temps, ce n'est qu'en 1988 que John C. Mitchell et Gordon Plotkin ont établi la théorie formelle sous le slogan : « Les types [de données] abstraits ont un type existentiel ». La théorie est un lambda-calcul typé du second ordre similaire au Système F , mais avec une quantification existentielle au lieu d'une quantification universelle.
Saisie progressive
De nombreux systèmes de typage statique, comme ceux de C et Java, exigent des déclarations de type : le programmeur doit associer explicitement chaque variable à un type spécifique. D'autres, comme celui de Haskell, effectuent une inférence de type : le compilateur déduit les types des variables en fonction de leur utilisation par le programmeur. Par exemple, étant donné une fonction qui additionne deux nombres, le compilateur peut déduire que ces nombres sont des nombres, puisque l'addition n'est définie que pour les nombres. Ainsi, tout appel à cette fonction ailleurs dans le programme, spécifiant un type non numérique (comme une chaîne de caractères ou une liste) comme argument, générera une erreur.f(x,y)xyxyf
Dans le code, les constantes et expressions numériques et textuelles peuvent impliquer, et impliquent souvent, un type dans un contexte particulier. Par exemple, une expression peut impliquer un type à virgule flottante , tandis qu'une autre peut impliquer une liste d'entiers (généralement un tableau) .3.14[1,2,3]
L'inférence de types est généralement possible si elle est calculable dans le système de types considéré. De plus, même si l'inférence n'est pas calculable en général pour un système de types donné, elle est souvent possible pour une grande partie des programmes réels. Le système de types de Haskell, une version de Hindley-Milner , est une restriction du Système Fω aux types polymorphes de rang 1, pour lesquels l'inférence de types est calculable. La plupart des compilateurs Haskell autorisent le polymorphisme de rang arbitraire comme extension, mais cela rend l'inférence de types impossible. (La vérification des types est cependant décidable , et les programmes de rang 1 bénéficient toujours de l'inférence de types ; les programmes polymorphes de rang supérieur sont rejetés sauf annotations de type explicites.)
Problèmes de décision
Système de types unifié
Certains langages, comme C# ou Scala, possèdent un système de types unifié. Cela signifie que tous les types C#, y compris les types primitifs, héritent d'un seul objet racine. Chaque type en C# hérite de la classe Object. D'autres langages, comme Java et Raku , possèdent un type racine, mais aussi des types primitifs qui ne sont pas des objets. Java fournit des types objets enveloppants qui coexistent avec les types primitifs, permettant ainsi aux développeurs d'utiliser indifféremment les types objets enveloppants ou les types primitifs non objets, plus simples. Raku convertit automatiquement les types primitifs en objets lors de l'accès à leurs méthodes.
Compatibilité : équivalence et sous-typage
Un vérificateur de types pour un langage statiquement typé doit s'assurer que le type de toute expression est cohérent avec le type attendu par le contexte dans lequel elle apparaît. Par exemple, dans une instruction d'affectation de la forme `x = x` , le type inféré de l'expression doit être cohérent avec le type déclaré ou inféré de la variable `x` . Cette notion de cohérence, appelée compatibilité , est spécifique à chaque langage de programmation.x:= eex
Si le type de `a` eet le type de `b` xsont identiques et que l'affectation est autorisée pour ce type, alors cette expression est valide. Ainsi, dans les systèmes de types les plus simples, la question de la compatibilité de deux types se réduit à celle de leur égalité (ou équivalence ). Cependant, les différents langages ont des critères différents pour déterminer quand deux expressions de type sont considérées comme désignant le même type. Ces différentes théories équationnelles des types varient considérablement ; deux cas extrêmes sont les systèmes de types structurels , où deux types décrivant des valeurs de même structure sont équivalents, et les systèmes de types nominatifs , où deux expressions de type syntaxiquement distinctes ne peuvent désigner le même type ( c'est-à -dire que les types doivent avoir le même « nom » pour être égaux).
Dans les langages avec sous-typage , la relation de compatibilité est plus complexe : si `T` Best un sous-type de ` AA`, alors une valeur de type `T` Bpeut être utilisée dans un contexte où une valeur de type ` A` Aest attendue ( covariance ), même si la réciproque n'est pas vraie. À l'instar de l'équivalence, la relation de sous-typage est définie différemment pour chaque langage de programmation, avec de nombreuses variations possibles. La présence de polymorphisme paramétrique ou ad hoc dans un langage peut également avoir des implications sur la compatibilité des types.