O que é uma explicação breve, mas completa, de um sistema do tipo puro / dependente?

32

Se algo é simples, deve ser completamente explicável com algumas palavras. Isso pode ser feito para o cálculo λ:

O cálculo λ é uma gramática sintática (basicamente uma estrutura) com uma regra de redução (o que significa que um procedimento de busca / substituição é aplicado repetidamente a todas as ocorrências de um padrão específico até que esse padrão não exista).

Gramática:

Term = (Term Term) | (λ Var . Term) | Var

Regra de redução:

((λ var body) term) -> SUBS(body,var,term)
    where `SUBS` replaces all occurrences of `var`
    by `term` in `body`, avoiding name capture.

Exemplos:

(λ 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

Embora um pouco informal, alguém poderia argumentar que isso é informativo o suficiente para um ser humano normal entender o cálculo λ como um todo - e são necessárias 22 linhas de remarcação. Estou tentando entender os sistemas de tipo puro / dependente usados ​​por Idris / Agda e projetos semelhantes, mas a explicação mais breve que pude encontrar foi Simply Easy - um ótimo artigo, mas que parece assumir muito conhecimento prévio (Haskell, indutivo). definições) que eu não tenho. Acho que algo mais breve e menos rico poderia eliminar algumas dessas barreiras. Portanto,

É possível dar uma breve e completa explicação dos sistemas do tipo puro / dependente, no mesmo formato que apresentei o cálculo λ acima?

MaiaVictor
fonte
4
As regras dos Pure Type Systems são muito breves. Simply Easy é sobre a implementação de tipos dependentes.
2
Portanto, não é "hostil" no sentido de ofensivo, mas no sentido que você acha que estou exigindo muito por não mostrar esforço suficiente para encontrar a resposta sozinho? Se for esse o caso, eu concordo que essa pergunta possa ser muito exigente, então talvez seja apenas ruim. Mas também há muito esforço por trás disso, você acha que devo editar nas minhas tentativas?
MaiaVictor
3
Também estou ofendido em nome dos meus co-autores que escreveram o texto de "Uma implementação tutorial de um cálculo lambda dependente de tipo", que substituiu "Simply Easy" como título de trabalho. Eu escrevi o kernel do código, que é um typechecker em <100 linhas de Haskell.
2
Então eu certamente me expressei mal. Adoro o jornal "Simply Easy" e o leio em todos os intervalos desde alguns dias atrás - é a única coisa no mundo que me deu uma sensação parcial. Estou começando a entender o assunto (e acredito que tentei) . Mas acho que é direcionado a um público com mais conhecimento do que eu tenho, e pode ser por isso que ainda estou tendo problemas para fazer parte disso. Nada a ver com a qualidade do papel, mas com minhas próprias limitações.
MaiaVictor
1
@pigworker e o código é minha parte favorita dele, exatamente porque (em relação à explicação em inglês) é uma explicação muito mais curta, porém completa, do todo, como pedi aqui. Você tem uma cópia do código que eu posso baixar?
MaiaVictor

Respostas:

7

aviso Legal

Isso é muito informal, como você solicitou.

A gramática

Em uma linguagem de tipo dependente, temos um fichário no nível do tipo e no nível do valor:

Term = * | (∀ (Var : Term). Term) | (Term Term) | (λ Var. Term) | Var

Termo bem digitado é um termo com tipo anexado, escreveremos t ∈ σou

σ
t

para indicar que o termo ttem tipo σ.

Regras de digitação

Por uma questão de simplicidade, exigimos isso em λ v. t ∈ ∀ (v : σ). τambos λe vinculamos a mesma variável ( vneste caso).

Regras:

t ∈ σ is well-formed if σ ∈ * and t is in normal form (0)

*            ∈ *                                                 (1)
∀ (v : σ). τ ∈ *             -: σ ∈ *, τ ∈ *                     (2)
λ v. t       ∈ ∀ (v : σ). τ  -: t ∈ τ                            (3)
f x          ∈ SUBS(τ, v, x) -: f ∈ ∀ (v : σ). τ, x ∈ σ          (4)
v            ∈ σ             -: v was introduced by ∀ (v : σ). τ (5)

Assim, *é "o tipo de todos os tipos" (1), forma tipos a partir de tipos (2), abstrações lambda têm tipos pi (3) e, se vintroduzido por ∀ (v : σ). τ, então vpossui o tipo σ(5).

"na forma normal" significa que realizamos o maior número possível de reduções usando a regra de redução:

Regra de redução "A"

(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ) ~> SUBS(b, v, t) ∈ SUBS(τ, v, t)
    where `SUBS` replaces all occurrences of `v`
    by `t` in `τ` and `b`, avoiding name capture.

Ou na sintaxe bidimensional em que

σ
t

significa t ∈ σ:

(∀ (v : σ). τ) σ    SUBS(τ, v, t)
                 ~>
(λ  v     . b) t    SUBS(b, v, t)

Só é possível aplicar uma abstração lambda a um termo quando o termo tiver o mesmo tipo que a variável no quantificador associado a todos. Em seguida, reduzimos a abstração lambda e o quantificador forall da mesma maneira que no cálculo lambda puro anterior. Após subtrair a parte do nível de valor, obtemos a regra de digitação (4).

Um exemplo

Aqui está o operador do aplicativo de funções:

∀ (A : *) (B : A -> *) (f : ∀ (y : A). B y) (x : A). B x
λ  A       B            f                    x     . f x

(abreviamos ∀ (x : σ). τse σ -> τif τnão menciona x)

fretorna B ypara qualquer fornecido ydo tipo A. Nós aplicamos fpara x, que é do tipo certo A, e substituto ypara xno depois ., assim f x ∈ SUBS(B y, y, x)~> f x ∈ B x.

Vamos agora abreviar o operador de aplicativo de função como appe aplicá-lo a si mesmo:

∀ (A : *) (B : A -> *). ?
λ  A       B          . app ? ? (app A B)

Coloco ?termos que precisamos fornecer. Primeiro, apresentamos e instanciamos explicitamente Ae B:

∀ (f : ∀ (y : A). B y) (x : A). B x
app A B

Agora precisamos unificar o que temos

∀ (f : ∀ (y : A). B y) (x : A). B x

que é o mesmo que

(∀ (y : A). B y) -> ∀ (x : A). B x

e o que app ? ?recebe

∀ (x : A'). B' x

Isto resulta em

A' ~ ∀ (y : A). B y
B' ~ λ _. ∀ (x : A). B x -- B' ignores its argument

(veja também O que é predicatividade? )

Nossa expressão (depois de renomear) se torna

∀ (A : *) (B : A -> *). ?
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

Desde para qualquer A, Be f(onde f ∈ ∀ (y : A). B y)

∀ (y : A). B y
app A B f

podemos instanciar Ae Bobter (para qualquer um fdo tipo apropriado)

∀ (y : ∀ (x : A). B x). ∀ (x : A). B x
app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) f

e a assinatura do tipo é equivalente a (∀ (x : A). B x) -> ∀ (x : A). B x.

Toda a expressão é

∀ (A : *) (B : A -> *). (∀ (x : A). B x) -> ∀ (x : A). B x
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

Ou seja,

∀ (A : *) (B : A -> *) (f : ∀ (x : A). B x) (x : A). B x
λ  A       B            f                    x     .
    app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B) f x

que depois de todas as reduções no nível de valor dá o mesmo appretorno.

Então, enquanto ele requer apenas alguns passos no cálculo lambda pura para obter appa partir de app app, em um ambiente digitada (e especialmente um dependente digitado) também precisa se preocupar com a unificação e as coisas se tornam mais complexas, mesmo com alguma conveniência inconsistentes ( * ∈ *).

Verificação de tipo

  • Se tfor *então t ∈ *por (1)
  • Se té ∀ (x : σ) τ, σ ∈? *, τ ∈? *(veja a nota sobre ∈?a seguir), em seguida, t ∈ *por (2)
  • Se tfor f x, f ∈ ∀ (v : σ) τpara alguns σe τ, x ∈? σentão , t ∈ SUBS(τ, v, x)por (4)
  • Se té uma variável v, vfoi introduzido até ∀ (v : σ). τentão t ∈ σpor (5)

Todas essas são regras de inferência, mas não podemos fazer o mesmo com lambdas (a inferência de tipo é indecidível para tipos dependentes). Portanto, para lambdas, verificamos ( t ∈? σ) em vez de inferir:

  • Se tfor λ v. be verificado contra ∀ (v : σ) τ, b ∈? τentãot ∈ ∀ (v : σ) τ
  • Se thouver outra coisa com a qual σcomparar, deduza o tipo de tuso da função acima e verifique se estáσ

A verificação de igualdade para tipos requer que eles estejam em formas normais, para decidir se ttem um tipo σ, primeiro verificamos se σhá um tipo *. Nesse caso, σé normalizável (paradoxo do módulo Girard) e é normalizado (portanto, σtorna-se bem formado por (0)). SUBStambém normaliza expressões para preservar (0).

Isso é chamado de verificação de tipo bidirecional. Com ele, não precisamos anotar todos os lambda com um tipo: se f xo tipo de ffor conhecido, xserá verificado o tipo de argumento frecebido em vez de inferido e comparado pela igualdade (que também é menos eficiente). Mas se ffor um lambda, ele exige uma anotação de tipo explícita (as anotações são omitidas na gramática e em todo lugar, você pode adicionar Ann Term Termou λ' (σ : Term) (v : Var)aos construtores).

Além disso, dê uma olhada no mais simples, mais fácil! blogpost.

user3237465
fonte
1
Destacamento "Mais simples, mais fácil".
A primeira regra de redução no forall parece estranha. Diferentemente dos lambdas, os foralls não devem ser aplicados de maneira bem digitada (certo?).
@chi, eu não entendo o que você está dizendo. Talvez minha notação seja ruim: a regra de redução diz (λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)~> SUBS(b, v, t) ∈ SUBS(τ, v, t).
user3237465
1
Acho a notação enganosa. Parece que você tinha duas regras: uma para o absurdo (∀ (v : σ). τ) t ~> ...e outra para o significativo (λ v. b) t ~> .... Gostaria de remover o primeiro e transformá-lo em um comentário abaixo.
1
A regra (1) contém sua conclusão como premissa. Você pode comparar a simplicidade do seu sistema com a versão bidirecional apenas quando tiver um sistema que funcione. Você pode dizer que mantém tudo normalizado, mas suas regras não.
24

Vamos tentar. Não vou me preocupar com o paradoxo de Girard, porque ele distrai as idéias centrais. Vou precisar introduzir algum mecanismo de apresentação sobre julgamentos, derivações e coisas do gênero.

Gramática

Termo :: = (Elim) | * (Var: Termo) → Termo | λVar↦Term

Elim :: = Termo: Termo | Var Elim Term

A gramática tem duas formas mutuamente definidas, "termos", que são a noção geral de coisa (tipos são coisas, valores são coisas), incluindo * (o tipo de tipos), tipos de funções dependentes e abstrações lambda, mas também incorporando " eliminações "(ou seja," usos "em vez de" construções "), que são aplicativos aninhados em que a coisa final na posição da função é uma variável ou um termo anotado com seu tipo.

Regras de redução

(λy↦t: (x: S) → T) s ↝ t [s: S / a]: T [s: S / x]

(t: T) ↝ t

A operação de substituição t [e / x] substitui todas as ocorrências da variável x pela eliminação e, evitando a captura de nomes. Para formar um aplicativo que possa reduzir, um termo lambda deve ser anotado por seu tipo para fazer uma eliminação . A anotação de tipo fornece à abstração lambda um tipo de "reatividade", permitindo que o aplicativo continue. Quando chegamos ao ponto em que não há mais aplicação acontecendo e o t: T ativo está sendo incorporado novamente na sintaxe do termo, podemos descartar a anotação de tipo.

Vamos estender a relação de redução closure através do fechamento estrutural: as regras se aplicam a qualquer lugar dentro dos termos e eliminações em que você pode encontrar algo que corresponda ao lado esquerdo. Escreva ↝ * para o fechamento reflexivo-transitivo (0 ou mais etapas) de ↝. O sistema de redução resultante é confluente neste sentido:

Se s ↝ * p e s ↝ * q, então existem alguns r tais que p ↝ * re q ↝ * r.

Contextos

Contexto :: = | Contexto, Var: Termo

Contextos são sequências que atribuem tipos a variáveis, crescendo à direita, que consideramos o fim "local", nos dizendo sobre as variáveis ​​associadas mais recentemente. Uma propriedade importante dos contextos é que sempre é possível escolher uma variável ainda não usada no contexto. Mantemos a invariante de que as variáveis ​​atribuídas aos tipos no contexto são distintas.

Acórdãos

Julgamento :: = Contexto ⊢ Termo possui Termo | Contexto ⊢ Elim é Termo

Essa é a gramática dos julgamentos, mas como lê-los? Para começar, ⊢ é o símbolo tradicional de "catraca", separando suposições de conclusões: você pode lê-lo informalmente como "diz".

G ⊢ T tem t

significa que, dado o contexto G, o tipo T admite o termo t;

G é e S

significa que, dado o contexto G, a eliminação e recebe o tipo S.

Os julgamentos têm estrutura interessante: zero ou mais entradas , um ou mais assuntos , zero ou mais resultados .

INPUTS                   SUBJECT        OUTPUTS
Context |- Term   has    Term
Context |-               Elim      is   Term

Ou seja, devemos propor os tipos de termos com antecedência e apenas verificá- los, mas sintetizamos os tipos de eliminações.

Regras de digitação

Eu os apresento em um estilo vagamente Prolog, escrevendo J -: P1; ...; Pn para indicar que o julgamento J se mantém se as premissas P1 a Pn também se mantêm. Uma premissa será outro julgamento ou uma reivindicação sobre redução.

Termos

G ⊢ T tem t -: T ↝ R; G ⊢ R tem t

G has * tem *

G ⊢ * tem (x: S) → T -: G ⊢ * tem S; G, z: S! - * tem T [z / x]

G ⊢ (x: S) → T tem λy↦t -: G, z: S ⊢ T [z / x] tem t [z / y]

G ⊢ T possui (e) -: G ⊢ e é T

Eliminações

G é e R -: G é e S; S ↝ R

G, x: S, G '⊢ x é S

G ⊢ fs é T [s: S / x] -: G ⊢ f é (x: S) → T; G ⊢ S tem s

E é isso!

Apenas duas regras não são direcionadas à sintaxe: a regra que diz "você pode reduzir um tipo antes de usá-lo para verificar um termo" e a regra que diz "você pode reduzir um tipo depois de sintetizá-lo a partir de uma eliminação". Uma estratégia viável é reduzir os tipos até que você exponha o construtor principal.

Esse sistema não está normalizando fortemente (por causa do Paradoxo de Girard, que é um paradoxo de auto-referência no estilo mentiroso), mas pode ser fortemente normalizado dividindo * em "níveis do universo", onde quaisquer valores que envolvem os tipos em níveis mais baixos têm tipos em níveis mais altos, impedindo a auto-referência.

Esse sistema, no entanto, possui a propriedade de preservação de tipo, nesse sentido.

Se G ⊢ T tem t e G ↝ * D e T ↝ * R e t ↝ r, então D ⊢ R tem r.

Se G ⊢ e é S e G ↝ * D e ef, então existe R tal que S ↝ * R e D and f é R.

Os contextos podem calcular, permitindo que os termos contidos nele sejam computados. Ou seja, se um julgamento é válido agora, você pode calcular suas entradas o quanto quiser e seu assunto em uma etapa e, em seguida, será possível calcular suas saídas de alguma forma para garantir que o julgamento resultante permaneça válido. A prova é uma simples indução em derivações de digitação, dada a confluência de -> *.

Obviamente, apresentei apenas o núcleo funcional aqui, mas as extensões podem ser bastante modulares. Aqui estão pares.

Termo :: = ... | (x: S) * T | s, t

Elim :: = ... | e.head | e.tail

(s, t: (x: S) * T) .head ↝ s: S

(s, t: (x: S) * T) .tail ↝ t: T [s: S / x]

G has * tem (x: S) * T -: G ⊢ * tem S; G, z: S * tem T [z / x]

G ⊢ (x: S) * T possui s, t -: G ⊢ S possui s; G ⊢ T [s: S / x] tem t

G ⊢ e.head é S -: G ⊢ e é (x: S) * T

G ⊢ e.tail é T [e.head / x] -: G ⊢ e é (x: S) * T

porco trabalhador
fonte
1
G, x:S, G' ⊢ x is S -: G' ⊬ x?
user3237465
1
@ user3237465 Não. Obrigado! Fixo. (Quando eu estava substituindo catracas de arte ASCII com html catracas (tornando-os invisíveis no meu celular; desculpe se o que está acontecendo em outros lugares) eu perdi essa.)
1
Oh, eu pensei que você estava apenas apontando o erro de digitação. A regra diz que, para cada variável no contexto, sintetizamos o tipo que o contexto a atribui. Ao introduzir contextos, eu disse: "Mantemos o invariante de que as variáveis ​​atribuídas aos tipos no contexto são distintas". então sombreamento é proibido. Você verá que toda vez que as regras estendem o contexto, elas sempre escolhem um "z" novo, que instancia qualquer ligação em que estamos pisando. Sombrear é anátema. Se você tiver o contexto x: *, x: x, o tipo do x mais local não estará mais ok porque é o x fora do escopo.
1
Eu só queria que você e os outros atendentes soubessem que estou voltando a esse tópico a cada intervalo do trabalho. Eu realmente quero aprender isso e, pela primeira vez, eu caí como se realmente conseguisse a maioria. O próximo passo será implementar e escrever alguns programas. Estou muito feliz por poder viver em uma época em que informações sobre tópicos tão maravilhosos estão disponíveis em todo o mundo para alguém como eu, e tudo isso graças a gênios como você, que dedicam algum tempo da vida deles para espalhar esse conhecimento, por grátis na internet. Desculpe novamente por formular minha pergunta muito mal, e obrigado!
amigos estão dizendo sobre maia
1
@ cod Sim, não há expansão. Para ver por que não é necessário, observe que as duas regras de computação permitem implantar a estratégia na qual você normaliza os tipos completamente antes de verificar os termos e também normaliza os tipos imediatamente após sintetizá-los das eliminações. Portanto, na regra em que os tipos devem corresponder, eles já são normalizados, portanto iguais no nariz se os tipos verificados e sintetizados "originais" forem conversíveis. Enquanto isso, restringir as verificações de igualdade apenas a esse local é aceitável por causa desse fato: se T é conversível em um tipo canônico, ele se reduz a um tipo canônico.
pigworker 01/09/16
8

A correspondência de Curry-Howard diz que existe uma correspondência sistemática entre sistemas de tipos e sistemas de prova na lógica. Tendo uma visão centrada no programador, você pode reformulá-la desta maneira:

  • Sistemas de prova lógica são linguagens de programação.
  • Esses idiomas são digitados estaticamente.
  • A responsabilidade do sistema de tipos em tal idioma é proibir programas que construam provas não fundamentadas.

Visto deste ângulo:

  • O cálculo lambda não digitado que você resume não possui um sistema de tipos significativo; portanto, um sistema de prova construído sobre ele não seria válido.
  • O cálculo lambda de tipo simples é uma linguagem de programação que possui todos os tipos necessários para criar provas sonoras na lógica sentencial ("se / então", "e", "ou", "não"). Mas seus tipos não são bons o suficiente para verificar as provas que envolvem quantificadores ("para todo x, ..."; "existe um x tal que ...").
  • O cálculo lambda tipicamente dependente possui tipos e regras que suportam lógica sentencial e quantificadores de primeira ordem (quantificação sobre valores).

Aqui estão as regras de dedução natural para lógica de primeira ordem, usando um diagrama da entrada da Wikipedia sobre dedução natural . Essas são basicamente as regras de um cálculo lambda mínimo tipicamente dependente também.

Dedução natural de primeira ordem

Observe que as regras possuem termos lambda. Eles podem ser lidos como os programas que constroem as provas das sentenças representadas por seus tipos (ou, mais sucintamente, apenas dizemos que programas são provas ). Regras de redução semelhantes fornecidas podem ser aplicadas a esses termos lambda.


Por que nos preocupamos com isso? Bem, antes de tudo, porque as provas podem se tornar uma ferramenta útil na programação, e ter uma linguagem que possa trabalhar com as provas como objetos de primeira classe abre muitos caminhos. Por exemplo, se sua função tiver uma pré-condição, em vez de anotá-la como um comentário, você poderá exigir uma prova dela como argumento.

Segundo, porque o mecanismo de sistema de tipos necessário para lidar com quantificadores pode ter outros usos em um contexto de programação. Em particular, linguagens de tipo dependente manipulam quantificadores universais ("para todos os x, ...") usando um conceito chamado tipos de funções dependentes - uma função em que o tipo estático do resultado pode depender do valor de tempo de execução do argumento.

Para dar uma aplicação muito simples a isso, escrevo código o tempo todo que precisa ler arquivos Avro que consistem em registros com estrutura uniforme - todos compartilham o mesmo conjunto de nomes e tipos de campos. Isso exige que eu:

  1. Codifique a estrutura dos registros no programa como um tipo de registro.
    • Vantagens: O código é mais simples e o compilador pode detectar erros no meu código
    • Desvantagem: O programa é codificado permanentemente para ler arquivos que concordam com o tipo de registro.
  2. Leia o esquema dos dados em tempo de execução, represente-os genericamente como uma estrutura de dados e use-o para processar registros genericamente
    • Vantagens: Meu programa não está codificado para apenas um tipo de arquivo
    • Desvantagens: O compilador não pode pegar tantos erros.

Como você pode ver na página do tutorial Avro Java , eles mostram como usar a biblioteca de acordo com as duas abordagens.

Com tipos de função dependentes, você pode comer e comer seu bolo, ao custo de um sistema de tipos mais complexo. Você pode escrever uma função que lê um arquivo Avro, extrai o esquema e retorna o conteúdo do arquivo como um fluxo de registros cujo tipo estático depende do esquema armazenado no arquivo . O compilador seria capaz de detectar erros onde, por exemplo, tentei acessar um campo nomeado que pode não existir nos registros dos arquivos que ele processará em tempo de execução. Doce, não é?

sacundim
fonte
1
Construir tipos em tempo de execução da maneira que você mencionou é algo muito legal que eu não pensei. Bastante doce, de fato! Obrigado pela resposta perspicaz.
perfil completo de MaiaVictor