Quindi ci ho pensato un po 'di più e ho fatto dei progressi. Ecco un primo tentativo di codificare il sistema deliziosamente semplice (ma incoerente) di Martin-Löf Set : Set
in uno stile combinatorio. Non è un buon modo per finire, ma è il posto più facile per iniziare. La sintassi di questa teoria dei tipi è semplicemente lambda-calcolo con annotazioni di tipo, tipi Pi e un set di universi.
La teoria del tipo di bersaglio
Per completezza, vi presento le regole. La validità del contesto dice solo che puoi costruire contesti dal vuoto aggiungendo nuove variabili che abitano Set
s.
G |- valid G |- S : Set
. |- valid G, x:S |- valid
E ora possiamo dire come sintetizzare i tipi per i termini in un dato contesto e come cambiare il tipo di qualcosa fino al comportamento computazionale dei termini che contiene.
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
In una piccola variazione rispetto all'originale, ho reso lambda l'unico operatore vincolante, quindi il secondo argomento di Pi dovrebbe essere una funzione che calcola il modo in cui il tipo restituito dipende dall'input. Per convenzione (ad esempio in Agda, ma purtroppo non in Haskell), l'ambito di lambda si estende verso destra il più lontano possibile, quindi puoi spesso lasciare le astrazioni senza parentesi quando sono l'ultimo argomento di un operatore di ordine superiore: puoi vedere che l'ho fatto quello con Pi. Il tuo tipo Agda (x : S) -> T
diventa Pi S \ x:S -> T
.
( Digressione . Le annotazioni di tipo su lambda sono necessarie se vuoi essere in grado di sintetizzare il tipo di astrazioni. Se passi al controllo del tipo come modus operandi, hai ancora bisogno di annotazioni per controllare un beta-redex come (\ x -> t) s
, poiché non hai modo per indovinare i tipi delle parti da quello del tutto. Consiglio ai designer moderni di controllare i tipi ed escludere i beta-redex dalla stessa sintassi.
( Digressione . Questo sistema è incoerente in quanto Set:Set
consente la codifica di una varietà di "paradossi bugiardi". Quando Martin-Löf ha proposto questa teoria, Girard gli ha inviato una codifica nel proprio Sistema U incoerente. Il successivo paradosso dovuto a Hurkens è il la più pulita costruzione tossica che conosciamo.)
Sintassi e normalizzazione del combinatore
Comunque, abbiamo due simboli extra, Pi e Set, quindi potremmo forse gestire una traduzione combinatoria con S, K e due simboli extra: ho scelto U per l'universo e P per il prodotto.
Ora possiamo definire la sintassi combinatoria non tipizzata (con variabili libere):
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 :.
Nota che ho incluso i mezzi per includere variabili libere rappresentate dal tipo a
in questa sintassi. Oltre ad essere un riflesso da parte mia (ogni sintassi degna di questo nome è una monade libera con return
variabili incorporanti e >>=
sostituzione di esecuzione), sarà utile rappresentare fasi intermedie nel processo di conversione dei termini con associazione alla loro forma combinatoria.
Ecco la normalizzazione:
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 esercizio per il lettore è definire un tipo esattamente per le forme normali e affinare i tipi di queste operazioni.)
Rappresentare la teoria dei tipi
Possiamo ora definire una sintassi per la nostra teoria dei tipi.
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)
Uso una rappresentazione dell'indice di de Bruijn alla maniera Bellegarde e Hook (resa popolare da Bird e Paterson). Il tipo Su a
ha un elemento in più rispetto a a
, e lo usiamo come il tipo di variabili libere sotto un legante, con Ze
come variabile appena associata ed Su x
essendo la rappresentazione spostata della vecchia variabile libera x
.
Tradurre i termini in combinatori
Fatto ciò, acquisiamo la solita traduzione, basata sull'astrazione delle parentesi .
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
Digitando i combinatori
La traduzione mostra il modo in cui utilizziamo i combinatori, il che ci fornisce un indizio su quali dovrebbero essere i loro tipi. U
e P
sono solo costruttori impostati, quindi, scrivendo tipi non tradotti e consentendo la "notazione Agda" per Pi, dovremmo avere
U : Set
P : (A : Set) -> (B : (a : A) -> Set) -> Set
Il K
combinatore viene utilizzato per elevare un valore di un tipo A
a una funzione costante rispetto a un altro tipo G
.
G : Set A : Set
K : (a : A) -> (g : G) -> A
Il S
combinatore viene utilizzato per sollevare le applicazioni su un tipo, dal quale possono dipendere tutte le parti.
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)
Se guardi il tipo di S
, vedrai che afferma esattamente la regola di applicazione contestualizzata della teoria dei tipi, quindi questo è ciò che lo rende adatto a riflettere il costrutto dell'applicazione. È il suo lavoro!
Abbiamo quindi applicazione solo per cose chiuse
f : Pi A B
a : A
f a : B a
Ma c'è un intoppo. Ho scritto i tipi dei combinatori nella teoria dei tipi ordinaria, non nella teoria dei tipi combinatori. Fortunatamente, ho una macchina che farà la traduzione.
Un sistema di tipo combinatorio
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
Quindi il gioco è fatto, in tutta la sua gloria illeggibile: una presentazione combinatoria di Set:Set
!
C'è ancora un piccolo problema. La sintassi del sistema ti dà modo di indovinare la G
, A
e B
parametri per S
e allo stesso modo per K
, solo dai termini. Di conseguenza, possiamo verificare algoritmicamente le derivazioni di battitura , ma non possiamo semplicemente controllare i termini del combinatore come avremmo potuto con il sistema originale. Ciò che potrebbe funzionare è richiedere l'input al tipografo per portare annotazioni di tipo sugli usi di S e K, registrando efficacemente la derivazione. Ma questa è un'altra lattina di vermi ...
Questo è un buon posto dove fermarsi, se sei stato abbastanza entusiasta di iniziare. Il resto è roba "dietro le quinte".
Generazione dei tipi di combinatori
Ho generato quei tipi combinatori usando la traduzione dell'astrazione delle parentesi dai termini rilevanti della teoria dei tipi. Per mostrare come ho fatto e rendere questo post non del tutto inutile, lasciami offrire la mia attrezzatura.
Posso scrivere i tipi dei combinatori, completamente astratti sui loro parametri, come segue. Uso la mia pratica pil
funzione, che combina Pi e lambda per evitare di ripetere il tipo di dominio, e piuttosto utilmente mi consente di utilizzare lo spazio delle funzioni di Haskell per legare le variabili. Forse puoi quasi leggere quanto segue!
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)
Con questi definiti, ho estratto i sottotermini aperti pertinenti e li ho passati alla traduzione.
Un toolkit di codifica de Bruijn
Ecco come costruire pil
. In primo luogo, definisco una classe di Fin
insiemi di ite, usata per le variabili. Ognuno di questi set ha un emb
edding che preserva il costruttore nel set sopra, più un nuovo top
elemento, e puoi distinguerli: la embd
funzione ti dice se un valore è nell'immagine di emb
.
class Fin x where
top :: Su x
emb :: x -> Su x
embd :: Su x -> Maybe x
Possiamo, ovviamente, istanziare Fin
per Ze
eSuc
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)
Ora posso definire minore o uguale, con un'operazione di indebolimento .
class (Fin x, Fin y) => Le x y where
wk :: x -> y
La wk
funzione dovrebbe incorporare gli elementi di x
come gli elementi più grandi di y
, in modo che le cose extra y
siano più piccole, e quindi in termini di indice di de Bruijn, legate più localmente.
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)
E una volta che hai risolto questo problema, un po 'di roba da poco fa il resto.
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 funzione di ordine superiore non ti dà solo un termine che rappresenta la variabile, ti dà una cosa sovraccaricata che diventa la rappresentazione corretta della variabile in qualsiasi ambito in cui la variabile è visibile. Cioè, il fatto che mi prenda la briga di distinguere i diversi ambiti per tipo fornisce al tipografo Haskell informazioni sufficienti per calcolare lo spostamento richiesto per la traduzione nella rappresentazione di de Bruijn. Perché tenere un cane e abbaiare da soli?