Si quelque chose est simple, alors il devrait être complètement explicable en quelques mots. Cela peut être fait pour le λ-calcul:
Le λ-calcul est une grammaire syntaxique (fondamentalement, une structure) avec une règle de réduction (ce qui signifie qu'une procédure de recherche / remplacement est appliquée à plusieurs reprises à chaque occurrence d'un modèle spécifique jusqu'à ce qu'il n'existe pas de modèle de ce type).
Grammaire:
Term = (Term Term) | (λ Var . Term) | Var
Règle de réduction:
((λ var body) term) -> SUBS(body,var,term) where `SUBS` replaces all occurrences of `var` by `term` in `body`, avoiding name capture.
Exemples:
(λ a . a) -> (λ a a) ((λ a . (λ b . (b a))) (λ x . x)) -> (λ b . (b (λ x x))) ((λ a . (a a)) (λ x . x)) -> (λ x . x) ((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x))) ((λ x . (x x)) (λ x . (x x))) -> never halts
Bien que quelque peu informel, on pourrait dire que c'est suffisamment informatif pour qu'un humain normal comprenne le λ-calcul dans son ensemble - et cela prend 22 lignes de démarque. J'essaie de comprendre les systèmes de type purs / dépendants utilisés par Idris / Agda et des projets similaires, mais l'explication plus brève que j'ai pu trouver était Simply Easy - un excellent article, mais qui semble supposer beaucoup de connaissances antérieures (Haskell, inductive définitions) que je n'ai pas. Je pense que quelque chose de plus bref, de moins riche pourrait éliminer certains de ces obstacles. Ainsi,
Est-il possible de donner une explication brève et complète des systèmes de type pur / dépendant, dans le même format que j'ai présenté le λ-calcul ci-dessus?
la source
Réponses:
Avertissement
C'est très informel, comme vous l'avez demandé.
La grammaire
Dans un langage typé de manière dépendante, nous avons un classeur au niveau du type ainsi qu'au niveau de la valeur:
Un terme bien typé est un terme avec un type attaché, nous écrirons
t ∈ σ
oupour indiquer que le terme
t
a un typeσ
.Règles de frappe
Par souci de simplicité, nous exigeons que dans les
λ v. t ∈ ∀ (v : σ). τ
deuxλ
et∀
lier la même variable (v
dans ce cas).Règles:
Ainsi,
*
est "le type de tous les types" (1),∀
forme les types à partir des types (2), les abstractions lambda ont des pi-types (3) et siv
est introduit par∀ (v : σ). τ
, alorsv
a le typeσ
(5)."sous forme normale" signifie que nous effectuons autant de réductions que possible en utilisant la règle de réduction:
"La" règle de réduction
Ou en syntaxe bidimensionnelle où
signifie
t ∈ σ
:Il n'est possible d'appliquer une abstraction lambda à un terme que lorsque le terme a le même type que la variable dans le quantificateur forall associé. Ensuite, nous réduisons à la fois l'abstraction lambda et le quantificateur forall de la même manière que dans le calcul lambda pur auparavant. Après avoir soustrait la partie du niveau de valeur, nous obtenons la règle de typage (4).
Un exemple
Voici l'opérateur d'application de fonction:
(nous abrégeons
∀ (x : σ). τ
àσ -> τ
siτ
ne mentionne pasx
)f
renvoieB y
pour touty
type fourniA
. Nous appliquonsf
àx
, qui est du type droitA
et substituery
àx
la∀
suite.
, doncf x ∈ SUBS(B y, y, x)
~>f x ∈ B x
.Abrévions maintenant l'opérateur d'application de fonction as
app
et appliquons-le à lui-même:Je place
?
pour les termes que nous devons fournir. D'abord, nous introduisons et instancions explicitementA
etB
:Maintenant, nous devons unifier ce que nous avons
ce qui est le même que
et ce qui
app ? ?
reçoitIl en résulte
(voir aussi Qu'est-ce que la prédicativité? )
Notre expression (après quelques renommage) devient
Depuis pour tout
A
,B
etf
(oùf ∈ ∀ (y : A). B y
)nous pouvons instancier
A
etB
obtenir (pour toutf
type approprié)et la signature de type est équivalente à
(∀ (x : A). B x) -> ∀ (x : A). B x
.L'expression entière est
C'est à dire
qui, après toutes les réductions au niveau de la valeur, donne le même
app
retour.Ainsi , alors qu'il ne nécessite que quelques étapes du calcul pur lambda pour obtenir
app
deapp app
, dans un cadre typé (et surtout un dactylographiées dépendamment) nous avons aussi besoin de se soucier de l' unification et les choses deviennent encore plus complexes avec une certaine commodité inconsitent (* ∈ *
).Vérification de type
t
est*
alorst ∈ *
par (1)t
est∀ (x : σ) τ
,σ ∈? *
,τ ∈? *
(voir la note sur∈?
ci - dessous) puist ∈ *
par (2)t
estf x
,f ∈ ∀ (v : σ) τ
pour certainsσ
etτ
,x ∈? σ
alorst ∈ SUBS(τ, v, x)
par (4)t
est une variablev
, av
été introduit d'ici∀ (v : σ). τ
làt ∈ σ
par (5)Ce sont toutes des règles d'inférence, mais nous ne pouvons pas faire de même pour les lambdas (l'inférence de type est indécidable pour les types dépendants). Donc, pour les lambdas, nous vérifions (
t ∈? σ
) plutôt que de déduire:t
estλ v. b
et vérifié contre∀ (v : σ) τ
,b ∈? τ
puist ∈ ∀ (v : σ) τ
t
c'est autre chose et vérifié,σ
déduisez le type d't
utilisation de la fonction ci-dessus et vérifiez si elle estσ
La vérification de l'égalité pour les types nécessite qu'ils soient sous des formes normales, donc pour décider si
t
a du type,σ
nous vérifions d'abord qu'ilσ
a du type*
. Si c'est le cas, alors ilσ
est normalisable (paradoxe de Modulo Girard) et il se normalise (doncσ
devient bien formé par (0)).SUBS
normalise également les expressions pour conserver (0).C'est ce qu'on appelle la vérification de type bidirectionnelle. Avec lui, nous n'avons pas besoin d'annoter chaque lambda avec un type: si
f x
le type def
est connu, ilx
estf
comparé au type d'argument reçu au lieu d'être déduit et comparé pour l'égalité (ce qui est également moins efficace). Mais s'ilf
s'agit d'un lambda, il nécessite une annotation de type explicite (les annotations sont omises dans la grammaire et partout, vous pouvez soit ajouterAnn Term Term
soitλ' (σ : Term) (v : Var)
aux constructeurs).Jetez également un œil à Simpler, Easier! blogpost.
la source
(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)
~>SUBS(b, v, t) ∈ SUBS(τ, v, t)
.(∀ (v : σ). τ) t ~> ...
l'autre pour le sens(λ v. b) t ~> ...
. Je supprimerais le premier et le transformerais en commentaire ci-dessous.Allons-y. Je ne m'occuperai pas du paradoxe de Girard, car il détourne l'attention des idées centrales. Je vais devoir introduire quelques mécanismes de présentation sur les jugements et les dérivations et autres.
Grammaire
La grammaire a deux formes mutuellement définies, les "termes" qui sont la notion générale de chose (les types sont des choses, les valeurs sont des choses), y compris * (le type de types), les types de fonction dépendants et les abstractions lambda, mais aussi l'intégration " éliminations "(c'est-à-dire" usages "plutôt que" constructions "), qui sont des applications imbriquées où la chose finalement en position de fonction est soit une variable soit un terme annoté de son type.
Règles de réduction
L'opération de substitution t [e / x] remplace chaque occurrence de la variable x par l'élimination e, évitant la capture de nom. Pour former une application qui peut réduire, un terme lambda doit être annoté par son type pour faire une élimination . L'annotation de type donne à l'abstraction lambda une sorte de "réactivité", permettant à l'application de se poursuivre. Une fois que nous avons atteint le point où il n'y a plus d'application et que le t: T actif est de nouveau incorporé dans la syntaxe du terme, nous pouvons supprimer l'annotation de type.
Étendons la relation de réduction by par la fermeture structurelle: les règles s'appliquent partout dans les termes et les éliminations que vous pouvez trouver quelque chose correspondant au côté gauche. Écrivez ↝ * pour la fermeture réflexive-transitive (0 ou plus étape) de ↝. Le système de réduction qui en résulte est confluent en ce sens:
Contextes
Les contextes sont des séquences qui affectent des types à des variables, croissant à droite, que nous considérons comme l'extrémité "locale", nous indiquant les variables liées les plus récentes. Une propriété importante des contextes est qu'il est toujours possible de choisir une variable qui n'est pas déjà utilisée dans le contexte. Nous maintenons l'invariant que les variables attribuées aux types dans le contexte sont distinctes.
Jugements
C'est la grammaire des jugements, mais comment les lire? Pour commencer, ⊢ est le symbole traditionnel du «tourniquet», séparant les hypothèses des conclusions: vous pouvez le lire de manière informelle comme «dit».
signifie que dans un contexte G donné, le type T admet le terme t;
signifie que compte tenu du contexte G, l'élimination e est donnée de type S.
Les jugements ont une structure intéressante: zéro ou plusieurs entrées , un ou plusieurs sujets , zéro ou plusieurs sorties .
Autrement dit, nous devons proposer les types de termes à l'avance et simplement les vérifier , mais nous synthétisons les types d'éliminations.
Règles de frappe
Je les présente dans un style vaguement prologue, en écrivant J -: P1; ...; Pn pour indiquer que le jugement J est valable si les locaux P1 à Pn le sont également. Une prémisse sera un autre jugement ou une demande de réduction.
Et c'est tout!
Seules deux règles ne sont pas axées sur la syntaxe: la règle qui dit "vous pouvez réduire un type avant de l'utiliser pour vérifier un terme" et la règle qui dit "vous pouvez réduire un type après l'avoir synthétisé à partir d'une élimination". Une stratégie viable consiste à réduire les types jusqu'à ce que vous ayez exposé le constructeur le plus haut.
Ce système n'est pas fortement normalisant (à cause du paradoxe de Girard, qui est un paradoxe d'auto-référence de type menteur), mais il peut être fortement normalisé en divisant * en "niveaux universels" où toutes les valeurs qui impliquent des types à des niveaux inférieurs eux-mêmes avoir des types à des niveaux plus élevés, empêchant l'auto-référence.
Ce système a cependant la propriété de la conservation de type, en ce sens.
Les contextes peuvent calculer en permettant aux termes qu'ils contiennent de calculer. Autrement dit, si un jugement est valide maintenant, vous pouvez calculer ses entrées autant que vous le souhaitez et son sujet en une étape, puis il sera possible de calculer ses sorties d'une manière ou d'une autre pour vous assurer que le jugement résultant reste valide. La preuve est une simple induction sur les dérivations de typage, étant donné la confluence de -> *.
Bien sûr, je n'ai présenté ici que le noyau fonctionnel, mais les extensions peuvent être assez modulaires. Voici des paires.
la source
G, x:S, G' ⊢ x is S -: G' ⊬ x
?La correspondance de Curry-Howard dit qu'il existe une correspondance systématique entre les systèmes de types et les systèmes de preuve en logique. En prenant une vue centrée sur le programmeur de cela, vous pouvez le refondre de cette façon:
Vu sous cet angle:
Voici les règles de déduction naturelle pour la logique du premier ordre, en utilisant un diagramme de l'entrée Wikipedia sur la déduction naturelle . Ce sont également les règles d'un calcul lambda minimal typé de manière dépendante.
Notez que les règles contiennent des termes lambda. Ceux-ci peuvent être lus comme les programmes qui construisent les preuves des phrases représentées par leurs types (ou plus succinctement, nous disons simplement que les programmes sont des preuves ). Des règles de réduction similaires que vous donnez peuvent être appliquées à ces termes lambda.
Pourquoi nous soucions-nous de cela? Eh bien, tout d'abord, parce que les preuves peuvent s'avérer être un outil utile dans la programmation, et avoir un langage qui peut fonctionner avec des preuves en tant qu'objets de première classe ouvre de nombreuses voies. Par exemple, si votre fonction a une condition préalable, au lieu de l'écrire en tant que commentaire, vous pouvez en demander une preuve en tant qu'argument.
Deuxièmement, parce que la machinerie du système de type nécessaire pour gérer les quantificateurs peut avoir d'autres utilisations dans un contexte de programmation. En particulier, les langages typés de manière dépendante gèrent les quantificateurs universels ("pour tous les x, ...") en utilisant un concept appelé types de fonction dépendants - une fonction où le type statique du résultat peut dépendre de la valeur d'exécution de l'argument.
Pour donner une application très piétonne à cela, j'écris du code tout le temps qui doit lire les fichiers Avro qui se composent d'enregistrements avec une structure uniforme - tous partagent le même ensemble de noms et de types de champs. Cela m'oblige à:
Comme vous pouvez le voir sur la page du didacticiel Avro Java , ils vous montrent comment utiliser la bibliothèque selon ces deux approches.
Avec les types de fonctions dépendants, vous pouvez avoir votre gâteau et le manger, au prix d'un système de type plus complexe. Vous pouvez écrire une fonction qui lit un fichier Avro, extrait le schéma et renvoie le contenu du fichier sous la forme d'un flux d'enregistrements dont le type statique dépend du schéma stocké dans le fichier . Le compilateur serait en mesure de détecter les erreurs lorsque, par exemple, j'ai essayé d'accéder à un champ nommé qui pourrait ne pas exister dans les enregistrements des fichiers qu'il traitera lors de l'exécution. Doux, hein?
la source