J'y ai donc réfléchi un peu plus et j'ai progressé. Voici une première tentative d'encodage du système délicieusement simple (mais incohérent) de Martin-Löf Set : Set
dans un style combinatoire. Ce n'est pas une bonne façon de terminer, mais c'est l'endroit le plus simple pour commencer. La syntaxe de cette théorie de type est juste un lambda-calcul avec des annotations de type, des types Pi et un ensemble d'univers.
La théorie du type de cible
Par souci d'exhaustivité, je vais présenter les règles. La validité de contexte indique simplement que vous pouvez créer des contextes à partir de vides en joignant de nouvelles variables habitant Set
s.
G |- valid G |- S : Set
. |- valid G, x:S |- valid
Et maintenant, nous pouvons dire comment synthétiser des types pour des termes dans un contexte donné, et comment changer le type de quelque chose jusqu'au comportement de calcul des termes qu'il contient.
G |- valid G |- S : Set G |- T : Pi S \ x:S -> Set
G |- Set : Set G |- Pi S T : Set
G |- S : Set G, x:S |- t : T x G |- f : Pi S T G |- s : S
G |- \ x:S -> t : Pi S T G |- f s : T s
G |- valid G |- s : S G |- T : Set
G |- x : S G |- s : T
Dans une petite variante de l'original, j'ai fait de lambda le seul opérateur de liaison, donc le deuxième argument de Pi devrait être une fonction calculant la façon dont le type de retour dépend de l'entrée. Par convention (par exemple dans Agda, mais malheureusement pas dans Haskell), la portée de lambda s'étend aussi loin que possible vers la droite, vous pouvez donc souvent laisser les abstractions sans crochets quand elles sont le dernier argument d'un opérateur d'ordre supérieur: vous pouvez voir que je l'ai fait cela avec Pi. Votre type Agda (x : S) -> T
devient Pi S \ x:S -> T
.
( Digression . Les annotations de type sur lambda sont nécessaires si vous voulez être capable de synthétiser le type d'abstractions. Si vous passez à la vérification de type comme modus operandi, vous avez toujours besoin d'annotations pour vérifier un beta-redex comme (\ x -> t) s
, car vous n'avez aucun moyen pour deviner les types des parties de celui de l'ensemble. Je conseille aux concepteurs modernes de vérifier les types et d'exclure les beta-redex de la syntaxe même.)
( Digression . Ce système est incohérent car il Set:Set
permet le codage d'une variété de "paradoxes de menteur". Lorsque Martin-Löf a proposé cette théorie, Girard lui a envoyé un codage de celle-ci dans son propre système U incohérent. la construction toxique la plus soignée que nous connaissons.)
Syntaxe et normalisation du combinateur
Quoi qu'il en soit, nous avons deux symboles supplémentaires, Pi et Set, donc nous pourrions peut-être gérer une traduction combinatoire avec S, K et deux symboles supplémentaires: j'ai choisi U pour l'univers et P pour le produit.
Nous pouvons maintenant définir la syntaxe combinatoire non typée (avec des variables libres):
data SKUP = S | K | U | P deriving (Show, Eq)
data Unty a
= C SKUP
| Unty a :. Unty a
| V a
deriving (Functor, Eq)
infixl 4 :.
Notez que j'ai inclus les moyens d'inclure des variables libres représentées par type a
dans cette syntaxe. En plus d'être un réflexe de ma part (toute syntaxe digne de ce nom est une monade libre avec des return
variables d'intégration et >>=
une substitution performante), il sera pratique de représenter des étapes intermédiaires dans le processus de conversion des termes avec liaison à leur forme combinatoire.
Voici la normalisation:
norm :: Unty a -> Unty a
norm (f :. a) = norm f $. a
norm c = c
($.) :: Unty a -> Unty a -> Unty a
C S :. f :. a $. g = f $. g $. (a :. g)
C K :. a $. g = a
n $. g = n :. norm g
infixl 4 $.
(Un exercice pour le lecteur consiste à définir un type pour exactement les formes normales et à affiner les types de ces opérations.)
Représenter la théorie des types
Nous pouvons maintenant définir une syntaxe pour notre théorie des types.
data Tm a
= Var a
| Lam (Tm a) (Tm (Su a))
| Tm a :$ Tm a
| Pi (Tm a) (Tm a)
| Set
deriving (Show, Functor)
infixl 4 :$
data Ze
magic :: Ze -> a
magic x = x `seq` error "Tragic!"
data Su a = Ze | Su a deriving (Show, Functor, Eq)
J'utilise une représentation d'index de Bruijn à la manière Bellegarde et Hook (telle que popularisée par Bird et Paterson). Le type Su a
a un élément de plus que a
, et nous l'utilisons comme type de variables libres sous un classeur, avec Ze
comme variable nouvellement liée et Su x
étant la représentation décalée de l'ancienne variable libre x
.
Traduire les termes en combinateurs
Et avec cela fait, nous acquérons la traduction habituelle, basée sur l' abstraction des crochets .
tm :: Tm a -> Unty a
tm (Var a) = V a
tm (Lam _ b) = bra (tm b)
tm (f :$ a) = tm f :. tm a
tm (Pi a b) = C P :. tm a :. tm b
tm Set = C U
bra :: Unty (Su a) -> Unty a
bra (V Ze) = C S :. C K :. C K
bra (V (Su x)) = C K :. V x
bra (C c) = C K :. C c
bra (f :. a) = C S :. bra f :. bra a
Saisir les combinateurs
La traduction montre la façon dont nous utilisons les combinateurs, ce qui nous donne un indice sur ce que devraient être leurs types. U
et ne P
sont que des constructeurs définis, donc, en écrivant des types non traduits et en permettant la "notation Agda" pour Pi, nous devrions
U : Set
P : (A : Set) -> (B : (a : A) -> Set) -> Set
Le K
combinateur est utilisé pour élever une valeur d'un certain type A
à une fonction constante sur un autre type G
.
G : Set A : Set
K : (a : A) -> (g : G) -> A
Le S
combinateur est utilisé pour élever des applications sur un type, dont toutes les parties peuvent dépendre.
G : Set
A : (g : G) -> Set
B : (g : G) -> (a : A g) -> Set
S : (f : (g : G) -> (a : A g) -> B g a ) ->
(a : (g : G) -> A g ) ->
(g : G) -> B g (a g)
Si vous regardez le type de S
, vous verrez qu'il énonce exactement la règle d'application contextualisée de la théorie des types, c'est donc ce qui le rend approprié pour refléter la construction d'application. C'est son travail!
Nous n'avons alors application que pour les choses fermées
f : Pi A B
a : A
f a : B a
Mais il y a un hic. J'ai écrit les types des combinateurs dans la théorie des types ordinaires, et non dans la théorie des types combinatoires. Heureusement, j'ai une machine qui fera la traduction.
Un système de type combinatoire
U : U
P : PU(S(S(KP)(S(S(KP)(SKK))(S(KK)(KU))))(S(KK)(KU)))
G : U
A : U
K : P[A](S(S(KP)(K[G]))(S(KK)(K[A])))
G : U
A : P[G](KU)
B : P[G](S(S(KP)(S(K[A])(SKK)))(S(KK)(KU)))
S : P(P[G](S(S(KP)(S(K[A])(SKK)))(S(S(KS)(S(S(KS)(S(KK)(K[B])))(S(KK)(SKK))))
(S(S(KS)(KK))(KK)))))(S(S(KP)(S(S(KP)(K[G]))(S(S(KS)(S(KK)(K[A])))
(S(S(KS)(KK))(KK)))))(S(S(KS)(S(S(KS)(S(KK)(KP)))(S(KK)(K[G]))))
(S(S(KS)(S(S(KS)(S(KK)(KS)))(S(S(KS)(S(S(KS)(S(KK)(KS)))
(S(S(KS)(S(KK)(KK)))(S(KK)(K[B])))))(S(S(KS)(S(S(KS)(S(KK)(KS)))(S(KK)(KK))))
(S(KK)(KK))))))(S(S(KS)(S(S(KS)(S(KK)(KS)))(S(S(KS)(S(KK)(KK)))
(S(S(KS)(KK))(KK)))))(S(S(KS)(S(S(KS)(S(KK)(KS)))(S(KK)(KK))))(S(KK)(KK)))))))
M : A B : U
M : B
Alors voilà, dans toute sa splendeur illisible: une présentation combinatoire de Set:Set
!
Il y a encore un petit problème. La syntaxe du système vous donne aucun moyen de deviner G
, A
et les B
paramètres pour S
et de même pour K
, juste des termes. En conséquence, nous pouvons vérifier les dérivations de typage de manière algorithmique, mais nous ne pouvons pas simplement vérifier les termes du combinateur comme nous le pouvions avec le système d'origine. Ce qui pourrait fonctionner est d'exiger que l'entrée du vérificateur de type porte des annotations de type sur les utilisations de S et K, enregistrant efficacement la dérivation. Mais c'est une autre boîte de vers ...
C'est un bon endroit pour s'arrêter, si vous avez eu envie de commencer. Le reste est des trucs "en coulisses".
Génération des types de combinateurs
J'ai généré ces types combinatoires en utilisant la traduction d'abstraction entre crochets à partir des termes de la théorie des types pertinents. Pour montrer comment je l'ai fait, et rendre ce post pas totalement inutile, permettez-moi de vous proposer mon équipement.
Je peux écrire les types des combinateurs, entièrement abstraits sur leurs paramètres, comme suit. J'utilise ma pil
fonction pratique , qui combine Pi et lambda pour éviter de répéter le type de domaine, et me permet plutôt utilement d'utiliser l'espace de fonctions de Haskell pour lier des variables. Peut-être que vous pouvez presque lire ce qui suit!
pTy :: Tm a
pTy = fmap magic $
pil Set $ \ _A -> pil (pil _A $ \ _ -> Set) $ \ _B -> Set
kTy :: Tm a
kTy = fmap magic $
pil Set $ \ _G -> pil Set $ \ _A -> pil _A $ \ a -> pil _G $ \ g -> _A
sTy :: Tm a
sTy = fmap magic $
pil Set $ \ _G ->
pil (pil _G $ \ g -> Set) $ \ _A ->
pil (pil _G $ \ g -> pil (_A :$ g) $ \ _ -> Set) $ \ _B ->
pil (pil _G $ \ g -> pil (_A :$ g) $ \ a -> _B :$ g :$ a) $ \ f ->
pil (pil _G $ \ g -> _A :$ g) $ \ a ->
pil _G $ \ g -> _B :$ g :$ (a :$ g)
Une fois ceux-ci définis, j'ai extrait les sous- termes ouverts pertinents et les ai exécutés tout au long de la traduction.
Une boîte à outils d'encodage de Bruijn
Voici comment construire pil
. Premièrement, je définis une classe d' Fin
ensembles ite, utilisés pour les variables. Chaque ensemble de ce type a un emb
edding préservant le constructeur dans l'ensemble ci-dessus, plus un nouvel top
élément, et vous pouvez les distinguer: la embd
fonction vous indique si une valeur est dans l'image de emb
.
class Fin x where
top :: Su x
emb :: x -> Su x
embd :: Su x -> Maybe x
Nous pouvons, bien sûr, instancier Fin
pour Ze
etSuc
instance Fin Ze where
top = Ze
emb = magic
embd _ = Nothing
instance Fin x => Fin (Su x) where
top = Su top
emb Ze = Ze
emb (Su x) = Su (emb x)
embd Ze = Just Ze
embd (Su x) = fmap Su (embd x)
Maintenant, je peux définir moins ou égal, avec une opération d' affaiblissement .
class (Fin x, Fin y) => Le x y where
wk :: x -> y
La wk
fonction doit incorporer les éléments de x
comme les plus grands éléments de y
, afin que les éléments supplémentaires y
soient plus petits, et donc en termes d'indice de Bruijn, liés plus localement.
instance Fin y => Le Ze y where
wk = magic
instance Le x y => Le (Su x) (Su y) where
wk x = case embd x of
Nothing -> top
Just y -> emb (wk y)
Et une fois que vous avez réglé le problème, un peu de bagarre de rang n fait le reste.
lam :: forall x. Tm x -> ((forall y. Le (Su x) y => Tm y) -> Tm (Su x)) -> Tm x
lam s f = Lam s (f (Var (wk (Ze :: Su x))))
pil :: forall x. Tm x -> ((forall y . Le (Su x) y => Tm y) -> Tm (Su x)) -> Tm x
pil s f = Pi s (lam s f)
La fonction d'ordre supérieur ne vous donne pas seulement un terme représentant la variable, elle vous donne une chose surchargée qui devient la représentation correcte de la variable dans n'importe quelle portée où la variable est visible. Autrement dit, le fait que je me donne la peine de distinguer les différentes portées par type donne au vérificateur de type Haskell suffisamment d'informations pour calculer le décalage requis pour la traduction en représentation de Bruijn. Pourquoi garder un chien et aboyer?