Tipos indutivos, funções recursivas, avaliação e o enunciado de teoremas em Lean
Christiano Braga · Mestrado em Sistemas e Computação · IME
Baseada no Hitchhiker's Guide to Logical Verification (LoVe), capítulo 2.
A Aula 1 leu uma prova como um termo cujo tipo é a proposição que ela prova. A mesma teoria de tipos classifica dados.
Um tipo como ℕ reúne valores, e um termo de tipo ℕ → ℕ é um programa. O #check abaixo se lê como o #check de um termo de prova, com tipos no lugar de proposições.
fun n => n + 1 : ℕ → ℕ#check fun n : ℕ => n + 1
fun n => n + 1 : ℕ → ℕEsta aula define tipos e funções e enuncia teoremas sobre eles. As provas esperam pela indução estrutural, nas próximas aulas.
O comando inductive define um tipo listando os seus construtores. O tipo contém exatamente os valores construídos por finitas aplicações de construtores, e nada mais.
Números naturais
namespace MyNat
inductive Nat : Type where
| zero : Nat
| succ : Nat → Nat
end MyNat
Listas sobre um tipo qualquer
namespace MyList
inductive List (α : Type) : Type where
| nil : List α
| cons : α → List α → List α
end MyList
Lean já fornece Nat e List, então cada reconstrução vive em um espaço de nomes.
Os construtores carregam dados de outros tipos. AExp é a sintaxe abstrata das expressões aritméticas, e a linguagem imperativa das últimas aulas a estende.
inductive AExp : Type where
| num : ℤ → AExp
| var : String → AExp
| add : AExp → AExp → AExp
| sub : AExp → AExp → AExp
| mul : AExp → AExp → AExp
| div : AExp → AExp → AExp
A expressão (x + 3) * y
((AExp.var "x").add (AExp.num 3)).mul (AExp.var "y") : AExp#check AExp.mul
(AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y")
((AExp.var "x").add (AExp.num 3)).mul (AExp.var "y") : AExpUma função sobre um tipo indutivo se define por casamento de padrões, uma equação por forma de construtor.
A recursão é estrutural quando cada chamada recursiva descasca um construtor. Lean aceita essas definições, já que elas terminam.
def add : ℕ → ℕ → ℕ
| m, Nat.zero => m
| m, Nat.succ n => Nat.succ (add m n)
def mul : ℕ → ℕ → ℕ
| _, Nat.zero => 0
| m, Nat.succ n => add m (mul m n)
def fib : ℕ → ℕ
| 0 => 0
| 1 => 1
| n + 2 => fib (n + 1) + fib n
def powerParam (m : ℕ) : ℕ → ℕ
| Nat.zero => 1
| Nat.succ n => mul m (powerParam m n)
Os padrões são mais ricos que construtores nus: n + 2 abrevia duas aplicações de succ. Um argumento que nenhuma equação inspeciona vai para a esquerda dos dois-pontos, como o m acima.
Uma definição pode receber um tipo como argumento, dado explicitamente em cada chamada, ou implicitamente entre chaves e inferido pelo elaborador. O prefixo @ restaura a forma explícita.
Argumento de tipo explícito
def append (α : Type) : List α → List α → List α
| List.nil, ys => ys
| List.cons x xs, ys => List.cons x (append α xs ys)
append : (α : Type) → List α → List α → List α#check @append
append : (α : Type) → List α → List α → List αImplícito, com a notação de listas
def appendPretty {α : Type} : List α → List α → List α
| [], ys => ys
| x :: xs, ys => x :: appendPretty xs ys
def reverse {α : Type} : List α → List α
| [] => []
| x :: xs => appendPretty (reverse xs) [x]
A notação escreve List.nil como [] e List.cons x xs como x :: xs, então a definição se lê como a sua própria especificação.
#eval executa um programa pelo compilador; #reduce normaliza um termo simbolicamente no kernel. Use #eval em escala.
Um ambiente leva nomes de variáveis a valores, e eval reduz uma expressão ao seu valor.
def eval (env : String → ℤ) : AExp → ℤ
| AExp.num i => i
| AExp.var x => env x
| AExp.add e₁ e₂ => eval env e₁ + eval env e₂
| AExp.sub e₁ e₂ => eval env e₁ - eval env e₂
| AExp.mul e₁ e₂ => eval env e₁ * eval env e₂
| AExp.div e₁ e₂ => eval env e₁ / eval env e₂
9#eval add 2 7
90#eval eval (fun _ => 7)
(AExp.div (AExp.var "y") (AExp.num 0))
0
A divisão por zero não falha. A divisão inteira é total, com x / 0 = 0. O comando #eval e a nossa função eval não têm relação, apesar dos nomes.
Uma equação cujos dois lados avaliam para o mesmo valor vale por rfl, o termo que a Aula 2 usou para n * n = 9 na testemunha 3.
Esta é a computação definicional. Ela resolve qualquer equação fechada, sem variáveis, e nada além.
example : add 2 7 = 9 := rfl
example : eval (fun _ => 7)
(AExp.div (AExp.var "y") (AExp.num 0)) = 0 := rfl
Em add m n = add n m as variáveis bloqueiam a computação, então a lei geral precisa de indução estrutural.
Um theorem é uma definição cujo tipo é uma proposição. Enunciá-lo não exige prova, e sorry fica onde a prova entrará.
namespace SorryTheorems
theorem declaration uses `sorry`add_comm (m n : ℕ) :
add m n = add n m := m:ℕn:ℕ⊢ add m n = add n m
All goals completed! 🐙
theorem declaration uses `sorry`reverse_reverse {α : Type} (xs : List α) :
reverse (reverse xs) = xs := α:Typexs:List α⊢ reverse (reverse xs) = xs
All goals completed! 🐙
end SorryTheorems
Lean assinala cada uso de sorry
'SorryTheorems.add_comm' depends on axioms: [sorryAx]#print axioms SorryTheorems.add_comm
'SorryTheorems.add_comm' depends on axioms: [sorryAx]
#print axioms informa em que uma prova se apoia. Uma prova por computação não depende de axioma nenhum.
Uma constante opaque tem tipo e nenhuma definição; um axiom afirma uma proposição sem prova nenhuma.
opaque a : ℤ
opaque b : ℤ
axiom a_less_b : a < b
Nada verifica um axioma, então um axioma inconsistente quebra silenciosamente todo o desenvolvimento. A disciplina enuncia axiomas apenas para discuti-los.
Defina sub sobre ℕ de modo que sub 3 7 = 0, já que ℕ não tem valores negativos.
A escolha das equações
A recursão descasca um succ de cada argumento, então o caso recursivo é m + 1, n + 1.
Dois casos base a interrompem. Subtrair zero devolve o primeiro argumento, e subtrair de zero devolve zero.
A primeira equação que casa vence, então 0, 0 cai na primeira delas.
def sub : ℕ → ℕ → ℕ
| m, 0 => m
| 0, _ => 0
| m + 1, n + 1 => sub m n
A verificação
4#eval sub 7 3
40#eval sub 3 7
0Cada verificação é também um teorema, já que os dois lados são fechados.
example : sub 3 7 = 0 := rfl
(x + 3) * yDesdobrando eval, uma equação por vez
eval env ((x + 3) * y) = eval env (x + 3) * eval env y = (eval env x + eval env 3) * env "y" = (env "x" + 3) * env "y" = (2 + 3) * 4 = 20
Cada passo é uma equação de eval: o caso mul, depois add, depois var e num.
O ambiente fornece as duas variáveis, e ℤ faz o resto.
A mesma computação em Lean
def someEnv : String → ℤ
| "x" => 2
| "y" => 4
| _ => 0
20#eval eval someEnv
(AExp.mul (AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y"))
20example : eval someEnv
(AExp.mul (AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y")) = 20 := rfl
Três afirmações sobre add, que recursa sobre o seu segundo argumento.
Resolvidas por rfl
example : add 2 7 = 9 := rfl
example (m : ℕ) : add m 0 = m := rfl
A primeira é fechada, então os dois lados computam para 9.
A segunda é geral, mas add m 0 casa a primeira equação de add qualquer que seja m, e reduz a m em um passo.
Não resolvida por rfl
namespace Worked
theorem declaration uses `sorry`zero_add (m : ℕ) : add 0 m = m := m:ℕ⊢ add 0 m = m
All goals completed! 🐙
end Worked
Aqui o segundo argumento é a variável, então equação alguma de add se aplica e o termo trava.
A afirmação é verdadeira, e prová-la exige indução estrutural sobre m, em uma aula adiante.
Quem decide qual lado computa é a forma da recursão, não a forma do enunciado.
Acrescente um elemento ao final de uma lista e relacione a operação com reverse.
A definição
def snoc {α : Type} : List α → α → List α
| [], y => [y]
| x :: xs, y => x :: snoc xs y
[1, 2, 3]#eval snoc [1, 2] 3
[1, 2, 3]O enunciado que ela sugere
namespace Worked
theorem declaration uses `sorry`reverse_cons {α : Type} (x : α) (xs : List α) :
reverse (x :: xs) = snoc (reverse xs) x := α:Typex:αxs:List α⊢ reverse (x :: xs) = snoc (reverse xs) x
All goals completed! 🐙
end Worked
reverse (x :: xs) desdobra para appendPretty (reverse xs) [x], e snoc (reverse xs) x trava na lista variável, então rfl falha.
Enunciar uma lei é gratuito; prová-la é o trabalho das próximas aulas. Escrever o enunciado primeiro é como um desenvolvimento cresce.
Dados e provas compartilham uma teoria de tipos: um programa é um termo cujo tipo é um tipo de função.
inductive define um tipo pelos seus construtores; os valores são exatamente as aplicações finitas de construtores.
As funções vêm do casamento de padrões com recursão estrutural, uma equação por forma de construtor.
Os argumentos de tipo são explícitos (α : Type) ou implícitos {α : Type}, inferidos pelo elaborador e restaurados por @.
#eval executa pelo compilador, #reduce normaliza no kernel, e rfl transforma computação em prova de qualquer equação fechada.
Um theorem enuncia uma proposição; sorry adia a prova e #print axioms expõe o adiamento como sorryAx.
As leis gerais sobre variáveis precisam de indução estrutural, o assunto de uma aula adiante.
Exercícios: veja as notas de aula.