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?
fonte
Respostas:
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:
Termo bem digitado é um termo com tipo anexado, escreveremos
t ∈ σ
oupara indicar que o termo
t
tem tipoσ
.Regras de digitação
Por uma questão de simplicidade, exigimos isso em
λ v. t ∈ ∀ (v : σ). τ
ambosλ
e∀
vinculamos a mesma variável (v
neste caso).Regras:
Assim,
*
é "o tipo de todos os tipos" (1),∀
forma tipos a partir de tipos (2), abstrações lambda têm tipos pi (3) e, sev
introduzido por∀ (v : σ). τ
, entãov
possui 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"
Ou na sintaxe bidimensional em que
significa
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:
(abreviamos
∀ (x : σ). τ
seσ -> τ
ifτ
não mencionax
)f
retornaB y
para qualquer fornecidoy
do tipoA
. Nós aplicamosf
parax
, que é do tipo certoA
, e substitutoy
parax
no∀
depois.
, assimf x ∈ SUBS(B y, y, x)
~>f x ∈ B x
.Vamos agora abreviar o operador de aplicativo de função como
app
e aplicá-lo a si mesmo:Coloco
?
termos que precisamos fornecer. Primeiro, apresentamos e instanciamos explicitamenteA
eB
:Agora precisamos unificar o que temos
que é o mesmo que
e o que
app ? ?
recebeIsto resulta em
(veja também O que é predicatividade? )
Nossa expressão (depois de renomear) se torna
Desde para qualquer
A
,B
ef
(ondef ∈ ∀ (y : A). B y
)podemos instanciar
A
eB
obter (para qualquer umf
do tipo apropriado)e a assinatura do tipo é equivalente a
(∀ (x : A). B x) -> ∀ (x : A). B x
.Toda a expressão é
Ou seja,
que depois de todas as reduções no nível de valor dá o mesmo
app
retorno.Então, enquanto ele requer apenas alguns passos no cálculo lambda pura para obter
app
a partir deapp 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
t
for*
entãot ∈ *
por (1)t
é∀ (x : σ) τ
,σ ∈? *
,τ ∈? *
(veja a nota sobre∈?
a seguir), em seguida,t ∈ *
por (2)t
forf x
,f ∈ ∀ (v : σ) τ
para algunsσ
eτ
,x ∈? σ
então ,t ∈ SUBS(τ, v, x)
por (4)t
é uma variávelv
,v
foi introduzido até∀ (v : σ). τ
entãot ∈ σ
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:t
forλ v. b
e verificado contra∀ (v : σ) τ
,b ∈? τ
entãot ∈ ∀ (v : σ) τ
t
houver outra coisa com a qualσ
comparar, deduza o tipo det
uso 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
t
tem 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)).SUBS
també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 x
o tipo def
for conhecido,x
será verificado o tipo de argumentof
recebido em vez de inferido e comparado pela igualdade (que também é menos eficiente). Mas sef
for 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 adicionarAnn Term Term
ouλ' (σ : Term) (v : Var)
aos construtores).Além disso, dê uma olhada no mais simples, mais fácil! blogpost.
fonte
(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)
~>SUBS(b, v, t) ∈ SUBS(τ, v, t)
.(∀ (v : σ). τ) t ~> ...
e outra para o significativo(λ v. b) t ~> ...
. Gostaria de remover o primeiro e transformá-lo em um comentário abaixo.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
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
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:
Contextos
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
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".
significa que, dado o contexto G, o tipo T admite o termo t;
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 .
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.
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.
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.
fonte
G, x:S, G' ⊢ x is S -: G' ⊬ x
?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:
Visto deste ângulo:
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.
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:
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 é?
fonte