Como ler a notação de sistemas de tipos?
(langdev.stackexchange.com)- Embora a notação de sistemas de tipos varie entre materiais, se você conhecer a estrutura comum de gramática, relação de tipagem e regras de inferência, consegue acompanhar a maioria das variações
- Um sistema de tipos opera sobre a sintaxe abstrata de uma linguagem, então primeiro é preciso distinguir por gramática os termos (
term) que têm tipo e os próprios tipos ⊢ e: τé um julgamento de tipagem que significa “a expressãoetem o tipoτ”, e deve ser lido como uma regra de inferência em que, se todas as condições acima da linha horizontal forem verdadeiras, a conclusão abaixo também é verdadeira- Quando entram variáveis e funções, um contexto é anexado, como em
Γ ⊢ e: τ, para rastrear os nomes e tipos das variáveis no escopo atual - Muitas regras de tipagem podem ser lidas como uma função recursiva de verificação de tipos, mas nem todo julgamento lógico se torna diretamente um algoritmo de verificação de tipos decidível
Notação de sistemas de tipos a partir da gramática
- Um sistema de tipos é um sistema sintático de uma linguagem de programação, isto é, um conjunto de regras que opera sobre a sintaxe abstrata da linguagem
- Uma explicação abrangente de um sistema de tipos normalmente primeiro apresenta a estrutura sintática tratada por meio de uma gramática e a expressa na notação BNF
- Mesmo na linguagem de tipos mais simples, a sintaxe se divide em duas grandes categorias
e: expressões (expression) que têm tipoτ: tipos (type) atribuídos às expressões
- A linguagem de exemplo tem como expressões literais booleanos, literais inteiros, expressões condicionais, operações aritméticas e operações de comparação, e usa
BooleIntcomo tipos - Dependendo do material, o símbolo de tipo pode aparecer como
t,T,σou outras letras gregas minúsculas em vez deτ, mas a estrutura geral é parecida - Linguagens mais complexas podem incluir mais categorias sintáticas, como comandos e padrões de casamento de padrões
Como ler relações de tipagem e julgamentos
- Depois de definir a gramática, normalmente se define uma relação de tipagem na forma
e : τ1 + 2 : Intsignifica “1 + 2tem tipoInt”1 + 2 : Boolsignificaria que a mesma expressão tem tipoBool, então está erradotrue + 2 : Intnão faz sentido nem como expressão, então não tem tipo algum
⊢ e : τé um julgamento de tipagem, e⊢pode ser lido como “a afirmação a seguir é verdadeira”- Uma regra sem nada acima da linha horizontal é um axioma (
axiom) sempre verdadeiro⊢ true : Bool⊢ false : Bool- regras de literais inteiros como
⊢ 0 : Int,⊢ 1 : Int,⊢ -1 : Int
- Uma regra com parte superior e inferior é uma regra de inferência
- se todas as condições acima forem verdadeiras, a conclusão abaixo é verdadeira
- se
e₁ee₂forem ambosInt, entãoe₁ + e₂éInt - se
e₁ee₂forem ambosInt, entãoe₁ < e₂éBool
Expressões condicionais e variáveis de tipo
- Os dois ramos de
if ... then ... else ...podem ter qualquer tipo, mas precisam ter o mesmo tipo entre siif true then 1 else 2é válidoif true then false else trueé válidoif true then 1 else trueé inválido
- Para expressar isso, a regra usa a variável
τpara representar o tipo dos ramos- a condição
e₁precisa serBool - o ramo
thene₂e o ramoelsee₃precisam ter o mesmo tipoτ - o tipo da expressão condicional inteira também será
τ
- a condição
- Ao aplicar a regra, é possível escolher qualquer tipo para
τ, mas dentro da mesma regra essa escolha precisa ser mantida de forma consistente
Lendo regras de inferência como algoritmos
- Essa notação vem da lógica formal, e a forma de especificar sistemas de tipos se parece especialmente com a dedução natural
- Essas regras são usadas para construir provas formais sobre propriedades do sistema e são importantes para demonstrar propriedades como segurança de tipos
- Um julgamento lógico nem sempre corresponde de imediato a um algoritmo de verificação de tipos decidível
- Em muitos casos,
⊢ e : τpode ser lido como uma função que obtém o tipoτa partir da expressãoe- normalmente há uma regra para cada forma de expressão da gramática
- cada regra de tipagem pode ser vista como um ramo de uma função recursiva de verificação de tipos
- A função
inferdo exemplo corresponde ao seguinte fluxotrueoufalseresultam emBool- literais inteiros resultam em
Int e₁ + e₂verifica se os resultados de inferência dos dois lados sãoInte então retornaInte₁ < e₂verifica se ambos os lados sãoInte então retornaBoolif e₁ then e₂ else e₃verifica se a condição éBool, depois se os tipos dos dois ramos são iguais, e então retorna esse tipo
- Mesmo quando não dá para traduzir diretamente em algoritmo, pensar em
ecomo entrada eτcomo saída ajuda a entender o fluxo de informação no julgamento
Variáveis e contexto
- Para lidar com linguagens de programação úteis, são necessárias variáveis, e o exemplo é estendido com funções para tomar a forma do simply typed lambda calculus
- A gramática estendida inclui
- variável
x - abstração de função
λx:τ. e - aplicação de função
e e - tipo de função
τ → τ
- variável
λx:τ. ecorresponde a(x:τ) => eem TypeScript, ef xcorresponde af(x)- Como o tipo de uma variável depende do contexto em que ela aparece, não dá para escrever a regra apenas na forma
⊢ x : ??? - Por isso, o julgamento de tipagem é estendido para
Γ ⊢ e : τΓé o contexto ou ambiente de tipos⊢separa as suposições contextuais à esquerda da afirmação a ser provada à direita- isso é lido como “sob o contexto
Γ, a expressãoetem tipoτ”
- Do ponto de vista algorítmico,
Γpode ser visto como uma entrada adicional no formatoMap<Variable, Type> - Formalmente, o contexto também é especificado como uma estrutura sintática
∅: contexto vazioΓ, x:τ: contexto com uma nova associação de variável- às vezes
•é usado como contexto vazio no lugar de∅
- Nessa representação, o contexto é mais próximo de uma association list que mapeia nomes de variáveis para tipos
O papel do contexto dentro das regras
- Muitas regras de tipagem não alteram o contexto e apenas o propagam
Γ ⊢ true : Bool- se
Γ ⊢ e₁ : InteΓ ⊢ e₂ : Int, entãoΓ ⊢ e₁ + e₂ : Int
- Nas regras de uso de variável e de expressão lambda, o contexto tem papel central
- se
x:τ ∈ Γ, entãoΓ ⊢ x : τ - se
Γ, x:τ₁ ⊢ e : τ₂, entãoΓ ⊢ (λx:τ₁. e) : τ₁ → τ₂
- se
- Ao verificar o tipo do corpo
ede uma expressão lambda, o contexto é estendido com a nova associaçãox:τ₁ - A regra de variável julga que, se houver uma associação da variável no contexto atual, então a variável tem aquele tipo
- O contexto é usado como um mecanismo de comunicação que transmite informação entre a regra da expressão lambda e a regra da variável
- Para simplificar, especificações desse tipo geralmente assumem que todas as variáveis já foram resolvidas e tornadas únicas, sem tratar sombreamento de variáveis
- A regra de aplicação de função verifica em conjunto o tipo da expressão função e o da expressão argumento
e₁precisa ter tipoτ₁ → τ₂e₂precisa ter tipoτ₁- o tipo da aplicação inteira
e₁ e₂passa a serτ₂
Outras notações frequentes
- Regras de inferência nem sempre são escritas apenas na vertical
- várias condições podem aparecer lado a lado na horizontal
- arranjos verticais e horizontais podem se misturar dentro da mesma regra
- As condições acima da linha horizontal normalmente são outros julgamentos, mas também podem ser condições laterais (
side condition) booleanas arbitráriasx:τ ∈ Γna regra de variável é um exemplo- em sistemas de tipos algorítmicos, pode aparecer
α fresh, significando queαdeve ser uma nova variável de tipo distinta das demais
Subtipagem
- Subtipagem é uma relação que trata a compatibilidade entre tipos de forma mais fraca do que igualdade estrita, e precisa ser definida explicitamente
- Em geral se escreve
τ₁ <: τ₂e se lê “τ₁é subtipo deτ₂” - Uma relação simples de subtipagem pode introduzir o tipo topo
⊤e o tipo fundo⊥τ <: τ: todo tipo é subtipo de si mesmoτ <: ⊤: todo tipo é subtipo de⊤⊥ <: τ:⊥é subtipo de todo tipo
- A primeira regra é a regra de reflexividade, frequentemente abreviada como
refl - Para permitir subtipagem, cada regra de tipagem que a aceita precisa usar explicitamente essa relação
- na regra de aplicação de função, pode-se permitir a aplicação se o tipo do argumento
τ₁for subtipo do tipo de parâmetroτ₂
- na regra de aplicação de função, pode-se permitir a aplicação se o tipo do argumento
Múltiplos contextos e verificação de tipos bidirecional
- Alguns sistemas de tipos definem julgamentos de tipagem que incluem mais de um contexto
- o segundo contexto costuma ser chamado de
Δ Γ;Δ ⊢ e : τé comum quando ambos os contextos funcionam como entradaΓ ⊢ e : τ ⊣ Δé comum quandoΔfunciona como saída
- o segundo contexto costuma ser chamado de
- O segundo contexto pode ter usos diferentes conforme o caso
- pode limitar certas variáveis para que só possam ser referenciadas dentro de determinadas expressões
- em linguagens de programação sensíveis a recursos, pode servir como contexto de saída para rastrear quais variáveis foram consumidas
- A verificação de tipos bidirecional é uma abordagem para fazer inferência de tipos não local limitada sem resolvedor de restrições
- Um sistema bidirecional divide o julgamento geral
Γ ⊢ e : τem dois julgamentos especializadosΓ ⊢ e ⇐ τ: julgamento de checagem (checking), que verifica se a expressãoetem o tipo esperadoτ; do ponto de vista algorítmico,τé entradaΓ ⊢ e ⇒ τ: julgamento de inferência (inference), usado quando não há informação de tipo esperada; do ponto de vista algorítmico,τé saída
- Os dois julgamentos são definidos de forma mutuamente recursiva, transmitindo informação de tipo nos dois sentidos
- Com essa abordagem, algumas anotações de tipo podem ser omitidas, e a regra de checagem para abstração lambda pode obter o tipo do parâmetro a partir do tipo de função esperado, permitindo omitir a anotação no vinculador da variável
1 comentários
Opiniões no Hacker News
Guy Steele já fez uma apresentação sobre esse tema no passado. Ele até deu nomes pesquisáveis a algumas notações, como diagramas bidimensionais de regras de inferência
Ele chama isso de metanotação da ciência da computação, mas, pessoalmente, parece mais próximo de teoria de linguagens de programação. https://m.youtube.com/watch?v=dCuZkaaou0Q
https://en.wikipedia.org/wiki/Guy_Steele Guy Steele
https://www.codemesh.io/codemesh2017/guy-l-steele palestra "A Cobbler's Child" na Code Mesh 2017
https://www.youtube.com/watch?v=qNPlDnX6Mio "A Cobbler's Child" (vídeo no YouTube)
https://www.youtube.com/watch?v=dCuZkaaou0Q "It's Time for a New Old Language" (vídeo no YouTube)
https://news.ycombinator.com/item?id=15473199 Discussão no HN
https://labs.oracle.com/pls/apex/f?p=94065:40150:0::::P40150... Slides
É estranho que pessoas que tentam falar com a máxima precisão sobre a arte humana mais precisa usem uma notação ambígua e inconsistente
Essa notação remonta a Frege. É difícil pesquisar se você não sabe o que procurar, mas este artigo parece ser um resumo bem bom: https://plato.stanford.edu/entries/frege-logic
O símbolo de catraca
|-já era usado, e a linha horizontal que nas aulas era chamada de “Fregescher Schlussstrich”, isto é, o traço de conclusão de Frege, parece ter sido originalmente parte da própria catraca e depois virou um elemento separado na notação modernaTypes and Programming Languages, de Benjamin C. Pierce, é um bom livro-texto que aborda esse conteúdo
Mesmo sendo formado em ciência da computação, ainda me confundo com a diferença de significado entre
|–e|=, e também com em qual nível de metassintaxe estão as variáveis usadasIronicamente, um dos motivos é que a própria notação não tem tipos explícitos
Para quem está em dúvida se deve ler: este texto explica a notação de sistemas de tipos que aparece em artigos de ciência da computação e, na prática, é uma introdução à notação BNF, regras de inferência etc. para sistemas de tipos
Parece um bom resumo
Entendo o conceito lógico de aplicação de tipos, mas, como não leio artigos de ciência da computação com frequência, a correspondência entre símbolos e significado não fixa bem na minha cabeça
Em um dos exemplos,
𝗍𝗋𝗎𝖾+2:𝖨𝗇𝗍significa “𝗍𝗋𝗎𝖾+2é do tipo𝖨𝗇𝗍”, mas o texto diz que isso é ainda mais estranho porque a própria expressão𝗍𝗋𝗎𝖾+2não faz sentido e não tem tipoSó que, em Python,
True + 2de fato é um inteiro e o valor é 3. Independentemente de se deveria ser assim, é assim que funcionaTrue + 2faz sentido, basta definir diretamente uma regra de julgamento que permita issoLógica e teoria de sistemas de tipos não se importam em si com quais axiomas e regras de inferência você usa; elas apenas permitem raciocinar sobre essas regras e suas interações. Por exemplo, você pode ter algo como
|- True : Bool,|- True : Int, ou, se quiser permitir apenas em certas expressões, pode construir uma regra que derive|- True + x: Inta partir de|- x : Inttrueé mapeado para 1, entãotrue+1=2True + 2não gere erro em Python ou C, isso continua sendo tolice, porque torna a semântica da linguagem mais difícil de inferir só para dar ao programador um pouco de açúcar sintáticoÓtimo. Eu tinha curiosidade sobre isso havia anos, mas não sabia quais termos pesquisar para aprender mais
Às vezes dá uma raiva quando alguém libera de graça um conhecimento esotérico que você aprendeu a duras penas ;) Eu realmente gostaria que um texto assim existisse quando aprendi isso. Espero que, com mais acessibilidade, haja menos linguagens bagunçadas
Ao ler o Ada Reference Manual, reconheci imediatamente esse tipo de sintaxe. Eu não sabia o nome, mas achei interessante vê-la em um caso de uso real, e a linguagem inteira é definida com essa notação
Ex.: https://ada-lang.io/docs/arm/AA-3/AA-3.7#syntax
O Ada Reference Manual explicita a notação que usa. Ele usa uma variação da Backus-Naur Form, e a seção linkada explica essa variação específica
Parece um bom lugar para evangelizar uma causa que decidi defender até o fim. Em formatos de anotação de tipo que usam dois-pontos, os espaços dos dois lados dos dois-pontos devem ser iguais
Para mim, há dois símbolos diferentes que por acaso têm a mesma forma, isto é, dois pontos. Um é o dois-pontos de rótulo, em que a parte da frente introduz a parte de trás, como em inglês, ou a esquerda é o rótulo da direita; entram aqui o início de bloco em Python, pares chave-valor e pares nome-valor de structs em C ou Rust
O outro é a anotação de tipo emprestada da matemática. Trata-se de uma relação binária, e relações binárias usam espaçamento igual à esquerda e à direita. Assim como não escrevemos
x= 1,x> y,x+ z, o natural seria escreverx : X, nãox: XQuando vejo
a: b, leio imediatamente como dois-pontos de rótulo; quando é anotação de tipo, sempre exige uma conversão mental extra, ainda que minúscula. Estou falando de sintaxe de linguagens de programação e, pessoalmente, prefiro muito maisx : XaX x[1] “Evangelion” é uma palavra bacana que vem de εὐαγγέλιον, ou seja, “boa notícia”. [2] https://en.wikipedia.org/wiki/Colon_(punctuation)#Usage_in_E...
f: X->Y, com mais espaço à direita dos dois-pontos; de 3 livros que verifiquei, 1 usava apenas essa notaçãoAlém disso, isso ainda se aproxima de rotulagem: é rotular uma aplicação de um tipo específico. O caso em que os dois-pontos são usados com um significado realmente diferente em matemática é quando servem como abreviação de
such that; por exemplo, em definições de conjunto como{ x : x \in IN and x | 2}, ou frequentemente junto com quantificadoresX x.x: Xé muito mais natural para mim, e também parece mais próximo do modo como os dois-pontos são usados na linguagem naturalHá uma proposição, e o que vem depois dos dois-pontos a explica em mais detalhes; como um tipo também é informação adicional sobre o que está à esquerda, encaixa perfeitamente
t[espaço]:[espaço]TA teoria dos tipos como um todo tem aspectos de uma bagunça inconsistente, mas este é um dos raros casos em que todo mundo é bastante consistente. Fiquei curioso para ver como eu escrevia na graduação, e vi que eu também deixava bonitinho e simétrico: https://dvt.name/logic/horse2.pdf
x: Xcorresponde ao uso em que “a explicação vem depois dos dois-pontos”Ou seja, algo como
variable x: It’s an X.age: intpode ser facilmente lido em inglês como “person’s age: an integer”Por isso, os dois-pontos nunca me incomodaram muito