Comment puis-je comprendre les règles Hindley-Milner?
Hindley-Milner est un ensemble de règles sous forme de calcul séquentiel (pas de déduction naturelle) qui démontre que nous pouvons déduire le type (le plus général) d'un programme de la construction du programme sans déclarations de type explicites.
Les symboles et la notation
Tout d'abord, expliquons les symboles et discutons de la priorité des opérateurs
- 𝑥 est un identifiant (de manière informelle, un nom de variable).
- : signifie est un type de (de manière informelle, une instance de ou "is-a").
- 𝜎 (sigma) est une expression qui est soit une variable soit une fonction.
- ainsi 𝑥: 𝜎 se lit " 𝑥 is-a 𝜎 "
- ∈ signifie "est un élément de"
- 𝚪 (Gamma) est un environnement.
- ⊦ (le signe d'assertion) signifie affirme (ou prouve, mais "affirme" contextuellement se lit mieux.)
- 𝚪 ⊦ 𝑥 : 𝜎 se lit donc "𝚪 affirme que 𝑥, est-a 𝜎 "
- 𝑒 est une instance (élément) réelle de type 𝜎 .
- 𝜏 (tau) est un type: basique, variable ( 𝛼 ), fonctionnel 𝜏 → 𝜏 ' ou produit 𝜏 × 𝜏' (le produit n'est pas utilisé ici)
- 𝜏 → 𝜏 ' est un type fonctionnel où 𝜏 et 𝜏' sont des types potentiellement différents.
𝜆𝑥.𝑒 signifie que 𝜆 (lambda) est une fonction anonyme qui prend un argument, 𝑥 , et renvoie une expression, 𝑒 .
soit 𝑥 = 𝑒₀ dans 𝑒₁ signifie dans l'expression, 𝑒₁ , remplacez 𝑒₀ partout où 𝑥 apparaît.
⊑ signifie que l'élément précédent est un sous-type (informellement - sous-classe) de ce dernier élément.
- 𝛼 est une variable de type.
- ∀ 𝛼.𝜎 est un type, ∀ (pour tous) les variables d'argument, 𝛼 , renvoyant l' expression 𝜎
- ∉ libre (𝚪) ne signifie pas un élément des variables de type libre de 𝚪 définies dans le contexte externe. (Les variables liées sont substituables.)
Tout au-dessus de la ligne est la prémisse, tout en dessous est la conclusion ( Per Martin-Löf )
Priorité, par exemple
J'ai pris certains des exemples les plus complexes des règles et inséré des parenthèses redondantes qui montrent la priorité:
- 𝑥: 𝜎 ∈ 𝚪 pourrait s'écrire (𝑥: 𝜎) ∈ 𝚪
𝚪 ⊦ 𝑥 : 𝜎 pourrait s'écrire 𝚪 ⊦ ( 𝑥 : 𝜎 )
𝚪 ⊦ let 𝑥 = 𝑒₀ in 𝑒₁ : 𝜏
est équivalent 𝚪 ⊦ (( let ( 𝑥 = 𝑒₀ ) in 𝑒₁ ): 𝜏 )
𝚪 ⊦ 𝜆𝑥.𝑒 : 𝜏 → 𝜏 ' est équivalent 𝚪 ⊦ (( 𝜆𝑥.𝑒 ): ( 𝜏 → 𝜏' ))
Ensuite, de grands espaces séparant les déclarations d'assertion et d'autres conditions préalables indiquent un ensemble de telles conditions préalables, et enfin la ligne horizontale séparant le principe de la conclusion fait apparaître la fin de l'ordre de priorité.
Les règles
Ce qui suit ici sont des interprétations anglaises des règles, chacune suivie d'un retraitement lâche et d'une explication.
Variable
Étant donné que 𝑥 est un type de 𝜎 (sigma), un élément de 𝚪 (Gamma),
concluez que 𝚪 affirme que 𝑥 est un 𝜎.
Autrement dit, dans 𝚪, nous savons que 𝑥 est de type 𝜎 parce que 𝑥 est de type 𝜎 dans 𝚪.
Il s'agit essentiellement d'une tautologie. Un nom d'identifiant est une variable ou une fonction.
Application de fonction
Étant donné que 𝚪 affirme 𝑒₀ est un type fonctionnel et 𝚪 affirme que 𝑒₁ est un 𝜏
conclure 𝚪 affirme que l'application de la fonction 𝑒₀ à 𝑒₁ est un type 𝜏 '
Pour reformuler la règle, nous savons que l'application de fonction retourne le type 𝜏 'car la fonction a le type 𝜏 → 𝜏' et obtient un argument de type 𝜏.
Cela signifie que si nous savons qu'une fonction renvoie un type et que nous l'appliquons à un argument, le résultat sera une instance du type que nous savons qu'elle renvoie.
Abstraction de fonction
Étant donné que 𝚪 et 𝑥 de type 𝜏 affirme 𝑒 est un type, 𝜏 '
conclure 𝚪 affirme une fonction anonyme, 𝜆 de 𝑥 renvoyant une expression, 𝑒 est de type 𝜏 → 𝜏'.
Encore une fois, quand nous voyons une fonction qui prend 𝑥 et renvoie une expression 𝑒, nous savons qu'elle est de type 𝜏 → 𝜏 'parce que 𝑥 (a 𝜏) affirme que 𝑒 est un 𝜏'.
Si nous savons que 𝑥 est de type 𝜏 et donc qu'une expression 𝑒 est de type 𝜏 ', alors une fonction de 𝑥 renvoyant l'expression 𝑒 est de type 𝜏 → 𝜏'.
Soit déclaration de variable
Étant donné 𝚪 assert 𝑒₀, de type 𝜎, et 𝚪 et 𝑥, de type 𝜎, les assert 𝑒₁ de type 𝜏
concluent 𝚪 affirme let
𝑥 = 𝑒₀ in
𝑒₁ de type 𝜏
En gros, 𝑥 est lié à 𝑒₀ dans 𝑒₁ (a 𝜏) parce que 𝑒₀ est un 𝜎, et 𝑥 est un 𝜎 qui affirme que 𝑒₁ est un 𝜏.
Cela signifie que si nous avons une expression 𝑒₀ qui est un 𝜎 (étant une variable ou une fonction), et un nom, 𝑥, également un 𝜎, et une expression 𝑒₁ de type 𝜏, alors nous pouvons substituer 𝑒₀ à 𝑥 partout où il apparaît à l'intérieur de 𝑒₁.
Instanciation
Étant donné que 𝚪 affirme 𝑒 de type 𝜎 'et 𝜎' est un sous-type de 𝜎
concluez 𝚪 affirme que 𝑒 est de type 𝜎
Une expression, 𝑒 est du type parent 𝜎 car l'expression 𝑒 est du sous-type 𝜎 ', et 𝜎 est le type parent de 𝜎'.
Si une instance est d'un type qui est un sous-type d'un autre type, alors c'est également une instance de ce super-type - le type plus général.
Généralisation
Étant donné que 𝚪 affirme que 𝑒 est un 𝜎 et que 𝛼 n'est pas un élément des variables libres de 𝚪,
concluez que 𝚪 affirme 𝑒, tapez pour toutes les expressions d'argument 𝛼 renvoyant une expression 𝜎
Donc en général, 𝑒 est tapé 𝜎 pour toutes les variables d'argument (𝛼) retournant 𝜎, car nous savons que 𝑒 est un 𝜎 et 𝛼 n'est pas une variable libre.
Cela signifie que nous pouvons généraliser un programme pour accepter tous les types d'arguments qui ne sont pas déjà liés dans la portée contenante (variables qui ne sont pas non locales). Ces variables liées sont substituables.
Mettre tous ensemble
Compte tenu de certaines hypothèses (comme l'absence de variables libres / non définies, un environnement connu), nous connaissons les types de:
- éléments atomiques de nos programmes (Variable),
- valeurs renvoyées par les fonctions (Function Application),
- constructions fonctionnelles (Function Abstraction),
- let bindings (Let Variable Declarations),
- types d'instances parent (Instanciation), et
- toutes les expressions (généralisation).
Conclusion
Ces règles combinées nous permettent de prouver le type le plus général d'un programme affirmé, sans exiger d'annotations de type.