Indução estrutural e provas de programas
Conjuntos recursivos, invariantes de palavras, listas, concatenação, reverse e map.
Na indução sobre naturais, o passo constrói a partir de . Na indução estrutural, seguimos os construtores do objeto: uma palavra pode nascer de outra palavra, uma lista pode nascer de uma cabeça e uma cauda, uma expressão pode nascer de duas subexpressões. Assumimos a ideia de base e passo da lição anterior, aplicada a cada construtor.
Uma definição indutiva descreve os objetos obtidos por um número finito de aplicações das regras. Não inclui objetos adicionais só porque não foram explicitamente proibidos.
Indução num conjunto recursivo
Se é o menor conjunto com e , queremos mostrar:
Para , fazemos indução estrutural:
- Base: é um múltiplo positivo de 3.
- Construtor: se e com , então , também múltiplo positivo de 3.
Isto prova que as regras nunca produzem outro tipo de valor, mas ainda falta a inclusão inversa. Por indução no natural positivo , e, se , então ; mostrar que todos os elementos construídos têm uma propriedade não prova que todos os objetos com essa propriedade se constroem, por isso verifica as duas direções numa igualdade de conjuntos.
Palavras e invariantes
Considera palavras sobre construídas por estas regras: a palavra vazia pertence a ; se , então ; se , então .
Para construir aabb, partimos de , aplicamos e obtemos ab. Aplicamos o mesmo construtor a ab e obtemos aabb. Não existe nas regras uma operação que acrescente apenas um a; o par exterior entra numa única aplicação.
Vamos provar que cada palavra de tem tantos como . O caso base tem zero de cada. Se tem de cada, e têm de cada. Se tem de cada e tem de cada, a concatenação tem de cada. Tratámos todos os construtores.
Uma propriedade que se conserva em cada construção chama-se invariante. Pode servir para mostrar que uma palavra não pertence ao conjunto: aaabb não pode pertencer a , porque tem contagens diferentes. Para mostrar que pertence, fornece uma construção a partir das bases.
Parênteses equilibrados
Se as regras são , e , todas as expressões têm igual número de aberturas e fechos, mas podemos provar uma propriedade mais forte: em cada prefixo, o número de fechos não excede o de aberturas. Define o saldo como aberturas menos fechos.
- Em
(), os prefixos têm saldos 0, 1 e 0. - Em , o primeiro parêntese acrescenta 1 ao saldo de cada prefixo de . O último fecha a abertura exterior.
- Em , um prefixo fica em ou atravessa e entra em . No segundo caso, o saldo total de é zero e o saldo restante é o do prefixo de .
Contagens totais iguais não chegam para esta propriedade: )( tem uma abertura e um fecho, mas o seu primeiro prefixo já tem saldo negativo. Para ver a diferença, seguimos o saldo após consumir símbolos de cada palavra. corresponde ao prefixo vazio; uma célula vazia indica que a palavra já terminou.
Saldo de (()) | Saldo de )( | |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 1 | −1 |
| 2 | 2 | 0 |
| 3 | 1 | |
| 4 | 0 |
O saldo final é zero nos dois casos. Só a primeira palavra mantém todos os prefixos com saldo não negativo. A definição de não inclui a palavra vazia: cada expressão começa em () e tem pelo menos dois símbolos, embora o prefixo vazio seja usado no teste do saldo.
Indução sobre listas finitas
Uma lista finita tem dois construtores: [], a lista vazia, e x:xs, uma cabeça x seguida da cauda xs. Para provar uma propriedade de todas as listas finitas:
- Prova o caso
[]. - Para uma cabeça arbitrária
xe uma cauda arbitráriaxs, assume a propriedade dexse prova-a parax:xs.
A hipótese pertence à cauda, não à lista inteira. Não usamos este princípio para listas infinitas de Haskell.
Em Haskell, f x aplica a função f a x; [x] abrevia x:[]; ++ concatena listas. As definições seguintes são equações para listas finitas:
length [] = 0
length (x:xs) = 1 + length xs
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]
Na concatenação, a recursão analisa a primeira lista. Por isso, induzir nessa lista costuma permitir aplicar logo a equação recursiva.
Comprimento de uma concatenação
Queremos length (xs ++ ys) = length xs + length ys para quaisquer listas finitas. Fazemos indução em xs, mantendo ys arbitrária.
Na base:
length ([] ++ ys)
= length ys definição de ++
= 0 + length ys aritmética
= length [] + length ys definição de length
No passo, a hipótese é length (xs ++ ys) = length xs + length ys:
length ((x:xs) ++ ys)
= length (x : (xs ++ ys)) definição de ++
= 1 + length (xs ++ ys) definição de length
= 1 + length xs + length ys hipótese de indução
= length (x:xs) + length ys definição de length
Nomear a equação usada em cada passagem permite conferir a prova sem executar o programa.
Uma função acrescenta dois elementos por passo
Considere-se uma função que coloca um separador depois de cada elemento da lista:
interleave x [] = []
interleave x (y:ys) = y : x : interleave x ys
interleave 0 [2,3] dá [2,0,3,0]. O separador também aparece depois do último elemento. Queremos provar length (interleave x ys) = 2 * length ys para todas as listas finitas, com x arbitrário.
A base dá length [] = 2 * length [], isto é, 0 = 0. No passo, toma uma cabeça y e uma cauda ys. A hipótese só fornece a igualdade para essa cauda:
length (interleave x (y:ys))
= length (y : x : interleave x ys) definição de interleave
= 1 + 1 + length (interleave x ys) duas aplicações de length
= 2 + 2 * length ys hipótese de indução
= 2 * (1 + length ys) distributividade
= 2 * length (y:ys) definição de length
A chamada recursiva recebe apenas ys; é aí que a hipótese se aplica. Mesmo quando x=y, foram acrescentadas duas posições, não uma: listas conservam repetições.
Associatividade da concatenação
Para (xs ++ ys) ++ zs = xs ++ (ys ++ zs), induzimos em xs, com ys,zs arbitrárias. Na base, ambos os lados reduzem a ys ++ zs; no passo:
((x:xs) ++ ys) ++ zs
= (x : (xs ++ ys)) ++ zs
= x : ((xs ++ ys) ++ zs)
= x : (xs ++ (ys ++ zs)) hipótese de indução
= (x:xs) ++ (ys ++ zs)
Também precisamos da identidade à direita xs ++ [] = xs. A base é [] ++ [] = []; no passo, (x:xs) ++ [] = x:(xs ++ []) = x:xs pela hipótese.
Reverse de uma concatenação
O lema é:
reverse (xs ++ ys) = reverse ys ++ reverse xs
Na base xs=[], o lado esquerdo é reverse ys; o direito é reverse ys ++ [], igual pelo lema da identidade à direita. No passo:
reverse ((x:xs) ++ ys)
= reverse (x : (xs ++ ys))
= reverse (xs ++ ys) ++ [x]
= (reverse ys ++ reverse xs) ++ [x] hipótese de indução
= reverse ys ++ (reverse xs ++ [x]) associatividade de ++
= reverse ys ++ reverse (x:xs)
É este lema que falta quando tentamos provar que inverter duas vezes recupera a lista:
reverse (reverse (x:xs))
= reverse (reverse xs ++ [x])
= reverse [x] ++ reverse (reverse xs) lema anterior
= [x] ++ xs hipótese de indução
= x:xs
A base é reverse (reverse []) = []. Os lemas são necessários porque a hipótese de indução só fala de reverse (reverse xs), não de reverse (reverse xs ++ [x]).
Fusão de map
map f aplica f a cada elemento. A composição (f . g) x é f (g x):
map f [] = []
map f (x:xs) = f x : map f xs
(f . g) x = f (g x)
Podemos provar map f (map g xs) = map (f . g) xs por indução em xs. A base dá [] dos dois lados. No passo:
map f (map g (x:xs))
= map f (g x : map g xs)
= f (g x) : map f (map g xs)
= (f . g) x : map (f . g) xs hipótese e definição de composição
= map (f . g) (x:xs)
A igualdade permite juntar duas travessias numa. A prova explica por que a transformação conserva os valores para todas as listas finitas, sem depender de exemplos escolhidos.
Provar que uma ordenação ordena
Considere-se a ordenação por inserção de elementos com uma ordem total:
insert x [] = [x]
insert x (y:ys)
| x <= y = x:y:ys
| otherwise = y : insert x ys
sort [] = []
sort (x:xs) = insert x (sort xs)
Para provar que sort xs está ordenada, primeiro provamos o lema: inserir x numa lista ordenada conserva a ordenação.
A lista vazia produz [x], ordenada. Para y:ys ordenada, se x <= y, o resultado começa em x,y e conserva a cauda. Se x > y, aplicamos a hipótese à inserção na cauda. Os valores de ys são todos pelo menos y, e o novo valor x também é maior que y; logo, a cabeça y continua antes de uma cauda ordenada.
Na prova de sort, a base é vazia. No passo, a hipótese garante que sort xs está ordenada, e o lema garante que insert x (sort xs) também está.
Conservação das multiplicidades
Para um valor , seja o número de ocorrências de na lista. Vale e
onde é 1 se e 0 no caso contrário. Queremos provar o lema
Fazemos indução em , com arbitrários. A base dá a lista , com exatamente ocorrências. No passo , se , a saída é e contamos . Se , a saída é ; pela hipótese, a contagem é . Ambas são a contagem original mais a ocorrência inserida.
Na indução sobre sort, a base conserva zero ocorrências. No passo, o lema acrescenta exatamente a ocorrência da cabeça às contagens de sort xs, que pela hipótese são as de xs. Assim, a contagem final coincide com a de x:xs para todo o .
Ordenação não é toda a correção: a função que devolve sempre [] também devolve uma lista ordenada. Para provar que é uma ordenação do conteúdo original, precisamos ainda de mostrar que conserva os elementos e as suas multiplicidades.
Exercícios
O conjunto gerado por uma regra
Seja o menor subconjunto de que contém 3 e tal que implica . Prova .
Pista
Uma igualdade de conjuntos exige as duas inclusões.
Mais uma pista
Usa indução estrutural para e indução em para .
Solução
Base estrutural: . Se , então , logo todo o elemento gerado está em . No sentido inverso, dá 3. Se , a regra dá . Assim cada elemento de pertence a .
Confere a tua resposta:
- Provei que tudo o que é gerado tem a forma indicada.
- Provei que cada 2k + 3 pode ser gerado.
- Usei o menor conjunto, não qualquer conjunto fechado pela regra.
Erros frequentes
Mostrar só que T contém 3 e é fechado pela regra permite concluir S ⊆ T, mas ainda não demonstra a inclusão inversa.
Uma transformação conserva o comprimento
Com , e a definição usual de length, prova para listas finitas.
Pista
O caso indutivo tem a forma . A imagem de x pode mudar, mas continua a ser um elemento.
Mais uma pista
Desenrola primeiro map e depois length; a hipótese aplica-se a .
Solução
Base: . Passo, com a hipótese para :
Logo a propriedade vale para todas as listas finitas e qualquer função .
Confere a tua resposta:
- Verifiquei a lista vazia.
- Fixei um elemento x e uma cauda xs arbitrários.
- Apliquei a hipótese só à cauda e terminei pela definição de length.
Erros frequentes
Não precisas de f ser injetiva. Valores repetidos continuam a ocupar posições distintas na lista.
Justificar uma ordenação
Considera esta ordenação por inserção, sobre listas finitas cujos elementos têm uma ordem total:
insert x [] = [x]
insert x (y:ys)
| x <= y = x:y:ys
| otherwise = y : insert x ys
sort [] = []
sort (x:xs) = insert x (sort xs)Prova por indução nas listas que, para cada valor ,
com e . O indicador é 1 quando a igualdade é verdadeira e 0 quando é falsa. Usa depois esse lema para provar que sort conserva todas as multiplicidades. Que outra propriedade falta para concluir a correção da ordenação?
Pista
Ordenada não significa necessariamente uma permutação da entrada.
Solução
O lema das contagens prova-se por indução em , com arbitrários. Na base, insert x [] = [x], que tem ocorrências de .
No passo :
- Se ,
insert x (y:zs) = x:y:zs. A contagem é , a contagem de mais . - Se , o resultado é
y : insert x zs. A hipótese para a cauda dá , o mesmo valor pedido.
Para sort, a base devolve [] e conserva todas as contagens. No passo, usando o lema e depois a hipótese para xs:
Falta ainda provar que a saída está ordenada. Isso usa o lema de que insert conserva a ordenação quando a lista recebida está ordenada. Conservar contagens não distingue sort da função identidade, que também conserva todos os elementos mas pode deixar a lista desordenada.
Confere a tua resposta:
- Exigi que insert preserve ordenação quando recebe uma lista ordenada.
- Provei o lema das contagens nos dois ramos de insert.
- Usei o lema na prova das contagens de sort e distingui-a da ordenação.
Erros frequentes
Provar apenas ordenação aceita programas que apagam elementos ou duplicam valores. A correção exige também conservação das multiplicidades.