Então, pensei um pouco mais e fiz alguns progressos. Aqui está uma primeira tentativa de codificar o sistema deliciosamente simples (mas inconsistente) de Martin-Löf Set : Set
em um estilo combinatório. Não é uma boa maneira de terminar, mas é o lugar mais fácil para começar. A sintaxe dessa teoria de tipo é apenas lambda-cálculo com anotações de tipo, tipos Pi e um conjunto de universo.
A Teoria do Tipo de Alvo
Para fins de integridade, apresentarei as regras. A validade de contexto apenas diz que você pode construir contextos a partir do vazio, juntando novas variáveis que habitam Set
s.
G |- valid G |- S : Set
-------------- ----------------------------- x fresh for G
. |- valid G, x:S |- valid
E agora podemos dizer como sintetizar tipos para termos em qualquer contexto dado, e como alterar o tipo de algo até o comportamento computacional dos termos que ele contém.
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
Em uma pequena variação do original, tornei lambda o único operador de ligação, portanto, o segundo argumento de Pi deve ser uma função que calcula a maneira como o tipo de retorno depende da entrada. Por convenção (por exemplo, em Agda, mas infelizmente não em Haskell), o escopo de lambda se estende para a direita tanto quanto possível, então você pode frequentemente deixar as abstrações sem colchetes quando elas são o último argumento de um operador de ordem superior: você pode ver que eu fiz isso com Pi. Seu tipo Agda (x : S) -> T
se torna Pi S \ x:S -> T
.
( Digressão . Anotações de tipo em lambda são necessárias se você quiser ser capaz de sintetizar o tipo de abstrações. Se você alternar para verificação de tipo como seu modus operandi, ainda precisará de anotações para verificar um beta-redex como (\ x -> t) s
, porque você não tem como para adivinhar os tipos das partes do todo. Aconselho os designers modernos a verificar os tipos e excluir beta-redexes da própria sintaxe.)
( Digressão . Este sistema é inconsistente, pois Set:Set
permite a codificação de uma variedade de "paradoxos mentirosos". Quando Martin-Löf propôs esta teoria, Girard enviou-lhe uma codificação em seu próprio Sistema U inconsistente. O paradoxo subsequente devido a Hurkens é o a construção tóxica mais limpa que conhecemos.)
Sintaxe e normalização do combinador
De qualquer forma, temos dois símbolos extras, Pi e Set, então talvez possamos fazer uma tradução combinatória com S, K e dois símbolos extras: eu escolhi U para o universo e P para o produto.
Agora podemos definir a sintaxe combinatória não tipada (com variáveis livres):
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 :.
Observe que incluí os meios para incluir variáveis livres representadas por tipo a
nesta sintaxe. Além de ser um reflexo de minha parte (toda sintaxe digna desse nome é uma mônada livre com return
variáveis de incorporação e >>=
substituição de desempenho), será útil representar estágios intermediários no processo de conversão de termos com ligação à sua forma combinatória.
Aqui está a normalização:
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 $.
(Um exercício para o leitor é definir um tipo exatamente para as formas normais e aprimorar os tipos dessas operações.)
Representando a Teoria do Tipo
Agora podemos definir uma sintaxe para nossa teoria de tipos.
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)
Eu uso uma representação de índice de Bruijn à maneira de Bellegarde e Hook (popularizada por Bird e Paterson). O tipo Su a
tem mais um elemento que a
, e nós o usamos como o tipo de variáveis livres em um fichário, com Ze
como a variável recém-ligada e Su x
sendo a representação deslocada da antiga variável livre x
.
Traduzindo Termos para Combinadores
E com isso feito, adquirimos a tradução usual, baseada na abstração de colchetes .
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
Digitando os Combinadores
A tradução mostra a maneira como usamos os combinadores, o que nos dá uma boa pista sobre quais deveriam ser seus tipos. U
e P
são apenas construtores de conjuntos, então, escrevendo tipos não traduzidos e permitindo "notação Agda" para Pi, devemos ter
U : Set
P : (A : Set) -> (B : (a : A) -> Set) -> Set
O K
combinador é usado para elevar um valor de algum tipo A
a uma função constante sobre algum outro tipo G
.
G : Set A : Set
-------------------------------
K : (a : A) -> (g : G) -> A
O S
combinador é usado para levantar aplicações sobre um tipo, do qual todas as partes podem depender.
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 você observar o tipo de S
, verá que ele afirma exatamente a regra de aplicação contextualizada da teoria de tipo, de modo que é o que o torna adequado para refletir a construção do aplicativo. Esse é o seu trabalho!
Então, temos aplicação apenas para coisas fechadas
f : Pi A B
a : A
--------------
f a : B a
Mas há um obstáculo. Eu escrevi os tipos de combinadores na teoria dos tipos comuns, não na teoria dos tipos combinatórios. Felizmente, tenho uma máquina que fará a tradução.
Um sistema de tipo combinatório
---------
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
Então aí está, em toda a sua glória ilegível: uma apresentação combinatória de Set:Set
!
Ainda há um pequeno problema. A sintaxe do sistema não oferece nenhuma maneira de adivinhar os parâmetros G
, A
e B
para S
e de forma semelhante K
, apenas a partir dos termos. Da mesma forma, podemos verificar as derivações de tipagem algoritmicamente, mas não podemos simplesmente verificar os termos do combinador como poderíamos com o sistema original. O que pode funcionar é exigir que a entrada para o verificador de tipo tenha anotações de tipo sobre os usos de S e K, registrando efetivamente a derivação. Mas essa é outra lata de vermes ...
Este é um bom lugar para parar, se você estiver interessado em começar. O resto é coisa de "bastidores".
Gerando os Tipos de Combinadores
Eu gerei esses tipos combinatórios usando a tradução de abstração de colchetes dos termos relevantes da teoria de tipo. Para mostrar como fiz isso, e tornar este post não totalmente inútil, deixe-me oferecer meu equipamento.
Posso escrever os tipos de combinadores, totalmente abstraídos de seus parâmetros, como segue. Eu uso minha pil
função prática , que combina Pi e lambda para evitar a repetição do tipo de domínio e, de forma bastante útil, me permite usar o espaço de funções de Haskell para vincular variáveis. Talvez você quase possa ler o seguinte!
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)
Com eles definidos, extraí os subtermos abertos relevantes e os executei na tradução.
A de Bruijn Encoding Toolkit
Veja como construir pil
. Em primeiro lugar, defino uma classe de Fin
conjuntos de itens, usados para variáveis. Cada um desses conjuntos tem um emb
edding que preserva o construtor no conjunto acima, além de um novo top
elemento, e você pode diferenciá-los: a embd
função informa se um valor está na imagem de emb
.
class Fin x where
top :: Su x
emb :: x -> Su x
embd :: Su x -> Maybe x
Podemos, é claro, instanciar Fin
para Ze
eSuc
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
Agora posso definir menos ou igual, com uma operação de enfraquecimento .
class (Fin x, Fin y) => Le x y where
wk :: x -> y
A wk
função deve incorporar os elementos de x
como os maiores elementos de y
, de modo que as coisas extras em y
sejam menores e, portanto, em termos de índice de de Bruijn, limitados mais localmente.
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
E uma vez que você tenha resolvido isso, um pouco de malandragem faz o 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)
A função de ordem superior não fornece apenas um termo que representa a variável, mas também uma coisa sobrecarregada que se torna a representação correta da variável em qualquer escopo onde a variável esteja visível. Ou seja, o fato de que me dou ao trabalho de distinguir os diferentes escopos por tipo dá ao inspetor de tipos Haskell informações suficientes para calcular o deslocamento necessário para a tradução para a representação de Bruijn. Por que ter um cachorro e latir sozinho?