J'ai récemment terminé un cours universitaire qui présentait Haskell et Agda (un langage de programmation fonctionnel typé dépendant), et je me demandais s'il était possible de remplacer le calcul lambda dans ces derniers par la logique combinatoire. Avec Haskell, cela semble possible en utilisant les combinateurs S et K, ce qui le rend sans point. Je me demandais quel était l'équivalent pour Agda. Par exemple, peut-on créer un langage de programmation fonctionnel typé de manière dépendante équivalent à Agda sans utiliser de variables?
En outre, est-il possible de remplacer d'une manière ou d'une autre la quantification par des combinateurs? Je ne sais pas si c'est une coïncidence mais la quantification universelle par exemple fait ressembler une signature de type à une expression lambda. Existe-t-il un moyen de supprimer la quantification universelle d'une signature de type sans changer sa signification? Par exemple dans:
forall a : Int -> a < 0 -> a + a < a
Peut-on exprimer la même chose sans utiliser un forall?
Réponses:
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 -------------- ----------------------------- x fresh for G . |- 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 -------------- x:S in G ----------------------------- S ={beta} T 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
devientPi 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 desreturn
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 -- requires first arg in normal form C S :. f :. a $. g = f $. g $. (a :. g) -- S f a g = f g (a g) share environment C K :. a $. g = a -- K a g = a drop environment n $. g = n :. norm g -- guarantees output in normal form 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)) -- Lam is the only place where binding happens | Tm a :$ Tm a | Pi (Tm a) (Tm a) -- the second arg of Pi is a function computing a Set | 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 quea
, et nous l'utilisons comme type de variables libres sous un classeur, avecZe
comme variable nouvellement liée etSu x
étant la représentation décalée de l'ancienne variable librex
.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 -- binds a variable, building a function bra (V Ze) = C S :. C K :. C K -- the variable itself yields the identity bra (V (Su x)) = C K :. V x -- free variables become constants bra (C c) = C K :. C c -- combinators become constant bra (f :. a) = C S :. bra f :. bra a -- S is exactly lifted application
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 neP
sont que des constructeurs définis, donc, en écrivant des types non traduits et en permettant la "notation Agda" pour Pi, nous devrionsU : Set P : (A : Set) -> (B : (a : A) -> Set) -> Set
Le
K
combinateur est utilisé pour élever une valeur d'un certain typeA
à une fonction constante sur un autre typeG
.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 ----------------- A ={norm} B 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 lesB
paramètres pourS
et de même pourK
, 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 unemb
edding préservant le constructeur dans l'ensemble ci-dessus, plus un nouveltop
élément, et vous pouvez les distinguer: laembd
fonction vous indique si une valeur est dans l'image deemb
.class Fin x where top :: Su x emb :: x -> Su x embd :: Su x -> Maybe x
Nous pouvons, bien sûr, instancier
Fin
pourZe
etSuc
instance Fin Ze where top = Ze -- Ze is the only, so the highest emb = magic embd _ = Nothing -- there was nothing to embed instance Fin x => Fin (Su x) where top = Su top -- the highest is one higher emb Ze = Ze -- emb preserves Ze emb (Su x) = Su (emb x) -- and Su embd Ze = Just Ze -- Ze is definitely embedded embd (Su x) = fmap Su (embd x) -- otherwise, wait and see
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 dex
comme les plus grands éléments dey
, afin que les éléments supplémentairesy
soient plus petits, et donc en termes d'indice de Bruijn, liés plus localement.instance Fin y => Le Ze y where wk = magic -- nothing to embed instance Le x y => Le (Su x) (Su y) where wk x = case embd x of Nothing -> top -- top maps to top Just y -> emb (wk y) -- embedded gets weakened and embedded
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?
la source
F
combinateur?F
agit différemment selon son premier argument: SiA
est un atome,M
etN
sont des termes etPQ
est un composite, alorsFAMN -> M
etF(PQ)MN -> NPQ
. Cela ne peut pas être représenté dans leSK(I)
calcul maisK
peut être représenté parFF
. Est-il possible d'étendre votre MLTT sans points avec cela?λ (A : Set) → λ (a : A) → a
de type(A : Set) → (a : A) → A
est traduit enS(S(KS)(KK))(KK)
, qui ne peut pas être utilisé dans un type où le type du deuxième argument dépend du premier argument.Je suppose que "Bracket Abstraction" fonctionne également pour les types dépendants dans certaines circonstances. Dans la section 5 de l'article suivant, vous trouverez quelques types K et S:
Coïncidences scandaleuses mais significatives
Syntaxe et évaluation de type sûr dépendant
Conor McBride, Université de Strathclyde, 2010
La conversion d'une expression lambda en une expression combinatoire correspond à peu près à la conversion d'une preuve de déduction naturelle en une preuve de style Hilbert.
la source