Je travaille sur un petit compilateur de calcul lambda qui a un système d'inférence de type Hindley-Milner qui fonctionne et prend désormais également en charge le let récursif (pas dans le code lié), ce qui, je crois, devrait suffire à le rendre complet .
Le problème est maintenant que je n'ai aucune idée de comment faire pour qu'il prenne en charge les listes, ou s'il les supporte déjà et j'ai juste besoin de trouver un moyen de les encoder. Je voudrais pouvoir les définir sans avoir à ajouter de nouvelles règles au système de type.
La façon la plus simple de penser à une liste x
est comme quelque chose qui est null
(ou la liste vide), ou une paire qui contient à la fois un x
et une liste de x
. Mais pour ce faire, je dois être en mesure de définir des paires et / ou, qui sont, je crois, le type de produit et de somme.
Semble que je peux définir des paires de cette façon:
pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)
Puisque pair
aurait le type a -> (b -> ((a -> (b -> x)) -> x))
, après avoir passé, disons, un int
et un string
, cela donnerait quelque chose avec le type (int -> (string -> x)) -> x
, qui serait la représentation d'une paire de int
et string
. Ce qui me dérange ici, c'est que si cela représente une paire, pourquoi cela n'est-il pas logiquement équivalent à, ni n'implique la proposition int and string
? Cependant, est équivalent à (((int and string) -> x) -> x)
, comme si je ne pouvais avoir que des types de produits comme paramètres de fonctions. Cette réponsesemblent résoudre ce problème, mais je n'ai aucune idée de ce que signifient les symboles qu'il utilise. De plus, si cela n'encode pas vraiment un type de produit, puis-je faire quelque chose avec les types de produits que je ne pourrais pas faire avec ma définition des paires ci-dessus (étant donné que je peux également définir des n-tuples de la même manière)? Sinon, cela ne contredirait-il pas le fait que vous ne pouvez pas exprimer la conjonction (AFAIK) en utilisant uniquement l'implication?
Et le type de somme? Puis-je en quelque sorte l'encoder en utilisant uniquement le type de fonction? Si oui, cela suffirait-il pour définir des listes? Ou bien, existe-t-il un autre moyen de définir des listes sans avoir à étendre mon système de type? Et sinon, quels changements devrais-je apporter si je veux que ce soit aussi simple que possible?
Veuillez garder à l'esprit que je suis un programmeur informatique, mais pas un informaticien ni un mathématicien et assez mauvais en lecture de notation mathématique.
Edit: je ne sais pas quel est le nom technique de ce que j'ai mis en œuvre jusqu'à présent, mais tout ce que j'ai est essentiellement le code que j'ai lié ci-dessus, qui est un algorithme de génération de contraintes qui utilise les règles pour les applications, les abstractions et les variables prises à partir de l'algorithme de Hinley-Milner, puis un algorithme d'unification qui obtient le type principal. Par exemple, l'expression \a.a
donnera le type a -> a
et l'expression \a.(a a)
lèvera une erreur de vérification se produit. En plus de cela, il n'y a pas exactement une let
règle mais une fonction qui semble avoir le même effet qui vous permet de définir des fonctions globales récursives comme ce pseudo-code:
GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
// Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType',
// meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically
tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
// The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
tempScope = new Scope(globalScope, nameOfFunction, tempType)
type = TypeOfTerm(term, tempScope) // Calculate the type of the term
Unify(tempType, type)
return type
// After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}
Le code obtient essentiellement le type du terme comme d'habitude, mais avant d'unifier, il ajoute le nom de la fonction en cours de définition avec un type factice dans la portée du type afin qu'il puisse être utilisé de l'intérieur de manière récursive.
Edit 2: Je viens de réaliser que j'avais également besoin de types récursifs, que je n'ai pas, pour définir une liste comme je le veux.
let func = \x -> (func x)
) vous obtenez ce que j'ai.Réponses:
Paires
Ce codage est le codage Eglise des paires. Des techniques similaires peuvent coder des booléens, des entiers, des listes et d'autres structures de données.
Dans le contexte→ ∨ ∧ ¬ ⟺
x:a; y:b
, le termepair x y
a le type(a -> b -> t) -> t
. L'interprétation logique de ce type est la formule suivante (j'utilise des notations mathématiques standard: est une implication, est ou, est et, est une négation; est l'équivalence des formules): Pourquoi « et ou »? Intuitivement, cara
b
t
pair
est une fonction qui prend une fonction en argument et l'applique à la paire. Cela peut se faire de deux manières: la fonction d'argument peut utiliser la paire, ou elle peut produire une valeur de typet
sans utiliser la paire du tout.pair
est un constructeur pour le type de paire etfirst
etsecond
sont des destructeurs. (Ce sont les mêmes mots utilisés dans la programmation orientée objet; ici, les mots ont une signification qui est liée à l' interprétation logique des types et des termes que je n'entrerai pas ici.) Intuitivement, les destructeurs vous permettent d'accéder à ce qui est dans l'objet et les constructeurs ouvrent la voie au destructeur en prenant comme argument une fonction qu'ils appliquent aux parties de l'objet. Ce principe peut être appliqué à d'autres types.Sommes
Le codage de l'Église d'une union discriminée est essentiellement double du codage de l'Église d'une paire. Lorsqu'une paire a deux parties qui doivent être assemblées et que vous pouvez choisir d'extraire l'une ou l'autre, vous pouvez choisir de construire l'union de deux manières et lorsque vous l'utilisez, vous devez autoriser les deux. Il y a donc deux constructeurs, et il y a un seul destructeur qui prend deux arguments.
Permettez-moi d'abréger le type
(a->t) -> (b->t) -> t
commeSUM(a,b)(t)
. Ensuite, les types de destructeurs et de constructeurs sont:Donc
Listes
Pour une liste, appliquez à nouveau le même principe. Une liste dont les éléments ont le type
a
peut être construite de deux manières: elle peut être une liste vide, ou elle peut être un élément (la tête) plus une liste (la queue). Par rapport aux paires, il y a une petite torsion en ce qui concerne les destructeurs: vous ne pouvez pas avoir deux destructeurs séparéshead
ettail
parce qu'ils ne fonctionneraient que sur des listes non vides. Vous avez besoin d'un destructeur unique, avec deux arguments, dont l'un est une fonction à 0 argument (c'est-à-dire une valeur) pour le cas nul, et l'autre une fonction à 2 arguments pour le cas contraire. Des fonctions commeis_empty
,head
ettail
peuvent être dérivées de cela. Comme dans le cas des sommes, la liste est directement sa propre fonction destructrice.Chacune de ces fonctions est polymorphe. Si vous travaillez les types de ces fonctions, vous remarquerez que ceT T1,…,Tn
cons
n'est pas uniforme: le type du résultat n'est pas le même que le type de l'argument. Le type du résultat est une variable - il est plus général que l'argument. Si vous enchaînez lescons
appels, les appels successifs pour construire une liste instancientcons
différents types. Ceci est crucial pour faire fonctionner les listes en l'absence de types récursifs. Vous pouvez ainsi créer des listes hétérogènes. En fait, les types que vous pouvez exprimer ne sont pas «liste de », mais «liste dont les premiers éléments sont de types ».Comme vous le supposez, si vous souhaitez définir un type qui ne contient que des listes homogènes, vous avez besoin de types récursifs. Pourquoi? Voyons le type d'une liste. Une liste est encodée comme une fonction qui prend deux arguments: la valeur à renvoyer sur des listes vides, et la fonction pour calculer la valeur à renvoyer sur une cellule contre. Soit
a
le type d'élément,b
le type de la liste etc
le type renvoyé par le destructeur. Le type d'une liste estRendre la liste homogène, c'est dire que si c'est une contre-cellule, la queue doit avoir le même type que l'ensemble, c'est-à-dire qu'elle ajoute la contrainte
Le système de type Hindley-Milner peut être étendu avec de tels types récursifs, et en fait les langages de programmation pratiques le font. Les langages de programmation pratiques ont tendance à interdire ces équations «nues» et nécessitent un constructeur de données, mais ce n'est pas une exigence intrinsèque de la théorie sous-jacente. Exiger un constructeur de données simplifie l'inférence de type et, en pratique, tend à éviter d'accepter des fonctions qui sont en fait boguées mais qui peuvent être typables avec une contrainte involontaire qui provoque une erreur de type difficile à comprendre lorsque la fonction est utilisée. C'est pourquoi, par exemple, OCaml accepte les types récursifs non protégés uniquement avec l'
-rectypes
option de compilation non par défaut . Voici les définitions ci-dessus dans la syntaxe OCaml, ainsi qu'une définition de type pour les listes homogènes utilisant la notation pourtypes récursifs aliasés :type_expression as 'a
signifie que le typetype_expression
est unifié avec la variable'a
.Plis
En regardant cela un peu plus généralement, quelle est la fonction qui représente la structure des données?
De manière générale, la structure des données est représentée comme sa fonction de repli . Il s'agit d'un concept général pour les structures de données: une fonction de repli est une fonction d'ordre supérieur qui traverse la structure de données. Il y a un sens technique dans lequel le pli est universel : tous les parcours de structure de données «génériques» peuvent être exprimés en termes de pli. Le fait que la structure de données puisse être représentée comme sa fonction de repliement le montre: tout ce que vous devez savoir sur une structure de données est de savoir comment la parcourir, le reste est un détail d'implémentation.
la source
t
et ignorer l'argument qui est censé prendrea
etb
(ce qui est exactement ce qui(a and b) or t
est dit). Et il semble que j'aurais le même genre de problème avec les sommes. Et aussi, sans types récursifs, je n'aurai pas de liste homogène. Donc, en quelques mots, dites-vous que je devrais ajouter des règles de somme, de produit et de type récursif pour obtenir des listes homogènes?case (right y) f g → g y
à la fin de votre section Sommes ?Vous pouvez représenter des types de somme en tant que types de produits avec des balises et des valeurs. Dans ce cas, nous pouvons tricher un peu et utiliser une balise pour représenter nul ou non, la deuxième balise représentant la paire tête / queue.
Nous définissons les booléens de la manière habituelle:
Une liste est alors une paire avec le premier élément comme booléen et le deuxième élément comme paire tête / queue. Quelques fonctions de liste de base:
la source