CORREÇÃO DE PROGRAMAS DRAFT 1

RAUL H.C.LOPES

1. Introdução

Estas notas introduzem um problema importante da arte de programação de computadores: correção de programas no paradigma imperativo. O conjunto de exemplos usados aborda em geral o tratamento de seqüências: pesquisa, soma, particionamento. Os problemas de particionamento motivarão a próxima seqüência de notas que trata de ordenação de seqüências.

Referências fundamentais sobre esses problemas são:

• correção de programas: [5], o marco inicial na área, [4], coleção artigos abordando algoritmos e provas interessantesm [6], texto claro e fácil de ler.

• ordenação: [9], a bíblia. Cada uma das próximas seções proporá um problema, cuja solução

será delineada. A cada solução apresentada, notação da linguagem de comandos guardados será introduzida e exercícios serão propostos.

Notação 1. O símbolo =^{△} é usado para introduzir nomes como abreviações de fórmulas, ou seja, introduzir nomes/símbolos via definiçãológica. Por exemplo,

P =^{△} 0 ≤ k ≤ m ∧ S = ∑i : 0 ≤ i < k : v.i

introduz o símbolo P como nome para a fórmula

0 ≤ k ≤ m ∧ S = ∑i : 0 ≤ i < k : v.i

Notação 2. As seguintes regras, essencialmente retiradas de [2] e usadas tanto por Curry ^{1} quanto por Church, poderão ser usadas para reduzir o número de parêntesis usados em expressões aritméticas e lógicas.

1Aliás, se você tem interesse sério em Ciência da Computação, cedo ou tarde deveria ler [2].

• operadores aritméticos são mais fortes do que operadores rela-

cionais. Assim 1 + 2 = 3

obviamente é uma abreviação de

(1 + 2) = 3

• dentre os operadores relacionais, negação é o mais forte, seguido de conjunção e disjunção e, por último, implicação e igualdade.

• ponto (.) e dois pontos (:) podem ser usados como marcadores para reduzir a número de parêntesis. Fim e início de expressão são marcadores com zero pontos. As regras básicas para seu uso neste texto são:

(1) um marcador com mais pontos tem mais força do que um marcador com menos pontos.

rev.xˆ ˆ ˆ ˆ ˆˆ y :=: revyˆ ˆ ˆ ˆ ˆˆ x

abrevia

(rev(xˆ ˆ ˆ ˆ ˆˆ y)) = (rev(yˆ ˆ ˆ ˆ ˆˆ x))

(2) um marcador à esquerda de um operador delimita uma expressão que se estende para a esquerda até um marcador de mais alta prioridade.

x + f.y

abrevia

x + f(y)

(3) um marcador à direita de um operador delimita uma expressão que se estende para a direita até um marcador de mais alta prioridade.

(4) a expressão delimitada por um marcador respeita os tipos da expressão envolvida.

rev.ε = ε

abrevia

rev(ε) = ε

e não

rev(ε = ε)

que violaria o fato de rev mapeia seqüências para seqüências.

(5) um ponto amarrado a um operador relacional tem mais força do que um ponto ligado operador funcional.

rev.x .=. rev.y

abrevia

rev(x) = rev(y)

(6) um marcador mais à esquerda tem mais prioridade do que um marcador mais á direita.

p ⇒. q ⇒. r ⇒ s

abrevia

p ⇒ (q ⇒ (⇒ s)

As provas listadas nestas notas e em outras presentadas ao longo do curso apresentam provas baseadas no formalismo da lógica equacional, usada também em [8]. Algumas dessas regras são apresentadas abaixo. Você pode usar os sistemas de prova, como Dedução Natural ou Sequent Calculus, ou sistema de Hilbert. O importante é que use um sistema de forma coerente e apresente referência para as fontes de suas regras para que não restem dúvidas sobre quaisquer passos de suas provas.

IMPORTANTE: Estas notas não substituem o conhecimento que você adquiriu na disciplina de Noções de lógica.

Definição 1. A seguir são apresentados teoremas da lógica de primeira ordem que são usados em diversas provas ao longo deste documento. Note que:

• Equivalência lógica de duas proposições é representada pela igualdade da mesma, ou seja,

p = q

é usado em lugar de

p ≡ q

• O símbolo ⊢ define uma relação de conseqüência.

p, q ⊢ r

abrevia: “ assumindo que p e q são verdades, então é possível provar r.

• Regras de inferência são freqüentemente introduzidas usando a notação:

\frac{Γ_{0} ⊢ α_{1}, Γ_{1} ⊢ α_{1}}{Λ ⊢ β}

que abrevia o raciocínio: se Γ_{0} é um conjunto de hipóteses apartir do qual se pode provar α_{0} e Γ_{1} é outro conjunto de hipóteses do qual se pode provar α_{1}, então do conjunto de hipóteses Λ é possível provar (deduzir) β.

Para exemplo mais concreto, observe a seguinte a regra, apresentada como Introdução da Conjunção no sistema de Dedução Natural.

\frac{⊢ p, ⊢ q}{⊢ p ∧ q}

que diz que

– se p é um teorema da lógica e q é um teorema da lógica, então (p ∧ q) é um teorema da lógica;

– ou, lendo de baixo para cima, para provar que (p ∧ q) é verdadeiro (ou seja, ⊢ (p ∧ q)), prove que p é verdadeiro (ou seja, ⊢ p) e prove que q é verdadeiro (ou seja, ⊢ q).

A lista a seguir apresenta alguns dos teoremas da lógica clássica usados. Ela é obviamente incompleta: por exemplo, não estão listados comutatividade de ∧ e ∨, associatividade, etc.

• Introdução de ∨ : ⊢ p ⇒ (p ∨ q)

• Eliminação de ∨ : ⊢ (p ⇒ r) ∧ (q ⇒ r) .⇒. (p ∨ q) ⇒ r

• Eliminação de ∧ : ⊢ p ∧ q ⇒ p

• Modus Ponens :⊢ p ∧ (p ⇒ q) ⇒ q

• Conseqüência : ⊢ (q ⇐ p) = (p ⇒ q)

• Leibniz : (e = f) ⊢ E[z : e] = E[z := f]

• de Morgan sobre ∧ : ⊢ ¬(p ∧ q) = ¬p ∨ ¬q

• de Morgan sobre ∨ : ⊢ ¬(p ∨ q) = ¬p ∧ ¬q

• de Morgan sobre cand : ⊢ ¬(p cand q) = ¬p cor ¬q

• de Morgan sobre cor : ⊢ ¬(p cor q) = ¬p cand ¬q

• distributividade de ∧sobre∨ : ⊢ r∧(p∨q) = (r∧p)∨(r∧q)

Técninas usuais de prova são codificadas pelas regras:

Teorema da dedução : \frac{P ⊢ Q}{⊢ P ⇒ Q}

Análise de caso : \frac{⊢ E[z := true], ⊢ E[z := false]}{⊢ E[z := P]}

Prova por contradição : \frac{⊢ ¬P ⇒ false}{⊢ P}

Contrapositiva : \frac{⊢ ¬Q ⇒ ¬P}{⊢ P ⇒ Q}

Implicação mútua : \frac{⊢ P ⇒ Q, ⊢ Q ⇒ P}{⊢ P = Q}

Introdução de ∧ : \frac{⊢ P, ⊢ Q}{⊢ P ∧ Q}

2. Pesquisa em uma seqüência

O problema aqui consiste em determinar se um elemento x ocorre em uma seqüência.

Uma seqüência de elementos de um tipo A é definida através do seguinte conjunto de axiomas:

(1) seqüência vazia: ε ∈ seq.A

(2) construtor: c ◁ x ∈ seq.A

(3) diferença de vazio:c ◁ x ≠ ε

(4) Igualdade: b ◁ x = c ◁ y .=. b = c ∧ x = y

Além disso, o seguinte priincípio da indução é usado para provar propriedades sobre seqüências.

(5) ∀x : x ∈ seq.A : P.x .=. P.ε ∧ ∀c, y : c ∈ A ∧ y ∈ seq.A : P.y ⇒ P.c ◁ y

Uma seqüência pode ser representada por uma lista ou por um vetor. Seus elemtos são todos de um mesmo tipo A. Neste texto, em princípio, todas as seqüências contêm inteiros, tipo Int. Serão abordadas soluções funcionais usando listas em Haskell e soluções imperativas, usando vetores e matrizes em uma linguagem de comandos guardados[5, 6].

2.1. Pesquisa Linear. Dada uma seqüência S de m inteiros e um elementos x, também inteiro, determine se x ∈ S.

2.1.1. Uma solução funcional. Uma solução em Haskell para o problema, assumindo que S é representada por uma lista é bastante trivial.

Motivação

x ∈ S = (x = S.0 ∨ x = S.1 ∨ … ∨ x = S.m − 1)

A solução

l i n s e a r c h x xs = f o l d l ( | |) False (map ( x==) xs )

Note que essa solução tem a desvantagem de passar pela seqüência duas vezes. A solução apresentada em sala de aula, que aplica a idéia, da associatividade à esquerda compara x com cada elemento da seqüência apenas uma vez.

Exercício 1. Considere ainda o problema de apresentar soluções funcionais para busca de um elemento em seqüência.

(1) Mostre que a solução acima implementa diretamente o resultado proposto ∃y : y ∈ S : x = y.

(2) Use os combinadores foldl e foldr para produzir soluções funcionais para o problema.

(3) Produza um algoritmo recursivo para resolver esse problema.

(4) Prove a correção das suas soluções.

2.1.2. A solução imperativa. Considere agora que a seqüência é representada por um vetor: s.0..m − 1 ∈ Int e x ∈ Int. Determine

R = (∃i : 0 ≤ i < m : x = s.i)

Notação 3. A notação associada a vetores é essencialmente emprestada da linguagem Ada:

• S.i ou S(i), elemento da posição i de S;

• S.0..m − 1 ou S(0..m − 1), vetor com m elementos, indexados de 0 a m;

• Dado um operador relacional ⊕,

B(i..j) ⊕ S(i..j) =^{△} ∀k : i ≤ k ≤ j : B.k⊕S.k

x ⊕ S(i..j) =^{△} ∀k : i ≤ k ≤ j : x⊕S.k

Motivação:

Relaxe o objetivo, e assuma que x ∉ S. No último passo da computação seria válido:

x ∉ S.0..i − 1 ∧ i = m

e o passo final consistiria em testar atingir determinar que não existe o que buscar porque i = m.

l i n s e r a c h (S , x , i ) i s

{m ≥ 0 ∧ S.0..m − 1 ∈ Int ∧ x ∈ Int} [[ i n i t i a l l y i =0

{invariant P } {bound T } do

G_{0} : m−i ≠ 0 cand x ≠ S . i → i := i +1 od {P ∧ ¬B}

; R:= i=m

{R .=. ∃j : 0 ≤ j < m : x = S.j} ]]

Figura 1. Pesquisa linear

Generalizando para qualquer passo e voltando à especificação original onde x pode ocorrer em S, o último passo da computação assumiria:

• x não ocorreu ainda na seqüência: x ∉ S.0..i − 1, para algum i;

• i é um índice válido na seqüência. Isso dá a seguinte candidata a invariante:

P =^{△} 0 ≤ i ≤ m ∧ ∀j : 0 ≤ i < j : x ≠ S.j

O progresso na computação ocorre pelo esgotamento da seqüência a pesquisar:

Variável derivada (ou bound function): T =^{△} m − i A computação termina quando

• seqüência está esgotada: m − i = 0

• seqüência não está esgotada, mas x = S.i; o que define o ponto-fixo do loop: (m − i = 0 cor x = S.i). Sua

negação, B, dá a condição para computação continuar:

B =^{△} (m − i ≠ 0 cand x ≠ S.i)

Note que

(P ∧ ¬B) ⇒ ∀k : 0 ≤ k < i : x ≠ S.k

o que, quando i = m, significa que x não ocorre em S.

O algoritmo aparece na figura 1. Note que ele aparece decorado com a invariante do loop e a variável derivada, que define o bound do loop.

Note que esta solução, ao contrário da funcional, só percorre a seqüência uma vez: mas… cada passo da computação usa duas comparações de inteiros, então…

A correção. A prova de correção do programa é composta dos seguintes passos:

• A correção parcial: a prova de que o programa, se termina, produz o resultado desejado. Essa prova será decomposta nas seguintes fases:

– prova de que P vale inicialmente;

– prova de que P é uma invariante do loop;

– prova de que (P ∧ ¬B) ⇒ ∀k : 0 ≤ k < i : x ≠ S.k

• A prova de que o programa termina. A prova será constituída

dos seguintes estágios:

– Prova de que P ∧ B ⇒ T > 0

– Prova de que o loop eventualmente termina.

• Prova de eventualmente o programa computa o R desejado.

Teorema 1. P vale inicialmente. Prova. Primeiro fator de P.

⟨initially⟩ i = 0 ∧ m ≥ 0

=⟨aritmética⟩

(0 ≤ i ≤ m)

Segundo fator de P.

⟨initially⟩ i = 0 ∧ m ≥ 0

⇒⟨Eliminação de ∧⟩

i = 0

⇒⟨lógica⟩

∀k : 0 ≤ k < i : x ≠ S.k

□

Teorema 2. P é uma invariante do loop. Prova. O objetivo é provar que

{P ∧ B}G_{0}{P }

Se P é válido e a guarda B é válida, G_{0} é executado e, depois, P ainda será válido.

⟨P depois é P′⟩ P′ = P [i := i + 1]

=⟨substituição de i⟩

0 ≤ i + 1 ≤ m ∧ ∀k : 0 ≤ k < i + 1 : x ≠ S.k

=⟨aritmética⟩

−1 ≤ i < m ∧ ∀k : 0 ≤ k < i + 1 : x ≠ S.k

=⟨lógica⟩

−1 ≤ i ≤ m ∧ m ≠ i ∧ ∀k : 0 ≤ k < i : x ≠ S.k ∧ x ≠ S.i

⇐⟨weakening⟩

(0 ≤ i ≤ m ∧ ∀k : 0 ≤ k < i : x ≠ S.k) ∧ (m − i ≠ 0 ∧ x ≠ S.i)

=⟨premissa do teorema⟩

P ∧ B

□

A prova acima consite em mostrar que

P ∧ B .⇒. wp(i := i + 1, P)

onde wp(α, P) é o transformador de predicados de Dijkstra[5].

Definição 2. A precondição mais simples (weakest precondition) da atribuição é dada por:

wp(x := e, P) = P [x := e]

Teorema 3. Enquanto a guarda B for válida, a variável derivada T é maior do que zero:

P ∧ B. ⇒ .T > 0

Prova.

10 RAUL H.C.LOPES

⟨initially P ⟩

0 ≤ i ≤ m ∧ ∀k : 0 ≤ k < i : x ≠ S.k

⇒ ⟨pela validade de B⟩ 0 ≤ i ≤ m ∧ ∀k : 0 ≤ k < i : x ≠ S.k ∧ m − i ≠ 0

=⟨eliminação da conjunção e aritmética por i ≤ m ∧ m − i ≠ 0⟩

i < m

=⟨aritmética⟩

0 < m − i

=⟨definição de T ⟩

0 < T

=T > 0
□

Teorema 4. Se B é válida, T decresce. Prova. Trivial: Se B é válida, i cresce e m − i decresce.

□

Teorema 5. O programa da figura 1 termina. Prova. Se inicialmente m = 0, B é falso e o programa termina de imediato. Se m > 0, existem dois casos:

(1) eventualmente x é igual a algum S.i e B se torna falso, terminando o loop e o programa, ou

(2) T decresce (provado anteriormente) e atinge zero, tornando B

falso, o que termina o loop e o programa. Teorema 6. Ao final do loop, quando ¬B é válido, x ∈ S se e somente se i ≠ m.

P ∧ ¬B. = .(i ≠ m) = ∃k : 0 ≤ k < m : x = S.k

Prova.

P ∧ ¬B
=0 ≤ i ≤ m ∧ x ∉ S.0..i − 1 ∧ ¬(m − i ≠ 0 cand x ≠ S.i)
=⟨de Morgan sobre cand ⟩
0 ≤ i ≤ m ∧ x ∉ S.0..i − 1 ∧ (m = i cor x = S.i)
=⟨distributividade de ∧ sobre cor ⟩
(0 ≤ i ≤ m ∧ x ∉ S.0..i − 1 ∧ m = i)
cor (0 ≤ i ≤ m ∧ x ∉ S.0..i − 1 ∧ m ≠ i ∧ x = S.i)
=(i = m ∧ ¬∃k : 0 ≤ k < m : x = S.k)
cor (0 ≤ i < m ∧ ∃k : 0 ≤ k < m : x = S.k)
=(i = m) = ¬∃k : 0 ≤ k < m : x = S.k)
□

Teorema 7. O programa da figura 1 termina. Prova. Os valores de m − i formam uma cadeia finita decrescente porque

(1) inicialmente m ≥ 0;

(2) m não muda;

(3) inicialmente i =;

(4) i é incrementado a cada passo.

Logo, eventualmente, m − i e loop termina, mesmo que x ∉ S.

□

Teorema 8. O programa da figura 1 termina e computa

R = x ∈ S

Prova. Provado nos teoremas acima.

□

O que foi feito até aqui? Um programa imperativo para o problema de pesquisa linear foi derivado no sentido de que sua construção obedeceu a uma seqüência que passou por:

• definir claramente o objetivo do mesmo;

• estabelecer claramente suas pré-condições;

• definir uma invariante para o loop da computação;

• estabelecer a condição de progresso e a variável derivada;

• definir a guarda do loop;

• definir o código do programa; • inverter este roteiro para encontrar a prova de correção do pro-

grama. Note que cada passo de cada uma das provas está lógicamente fun-

damentado. Ao longo deste curso, só serão aceitas provas apresentadas dessa forma. Construa para todos os outros programas desta nota (e deste curso) provas de correção que apresentem o mesmo rigor.

Exercício 2. Volte à idéia da relaxação: se for possível assumir que x ocorre na seqüência, pode-se contruir algoritmo para encontrar a posição de sua ocorrência em até m comparações. Use essa idéia para derivar um algoritmo que faz no máximo m + 2 comparações para determinar

(0 ≤ i < m ∧ x = S.i) cor (i = m ∧ x ∉ S)

2.2. Pesquisa bidimensional. Considere agora que uma seqüência bidimensional é dada: S(0..m − 1, 0..n − 1). O objetivo consiste em determinar outra vez

R = ∃i, j : 0 ≤ i < m ∧ 0 ≤ j < n : S.i.j = x

2.3. Pesquisa em seqüência ordenada. Dados x ∈ Int e S.0..m − 1 ∈ Int e ∀i, j : 0 ≤ i < j < m : S.i ≤ S.j, determine

present = ∃i : 0 ≤ i < m : x = S.i

O algoritmo exposto a seguir é apresentado por Dijkstra em [3].^{2} Para efeito de desenvolvimento, parece razoável relaxar o objetivo e

assuma que a seqüência se estende até a posição e que x < S.m.

Por outro lado, dado que o objetivo consiste em computar um quantificador existencial, parece intuitivo usar uma leitura construtiva desse quantificador e propor construir um i tal que:

• S.i = x determina a presença de x em S;

• S.i ≠ x garante que x ∉ S. O objetivo de determinar o valor de present passa a ser construído

em duas etapas:

• construir um i tal que S.i ≤ x < S.i + 1, proposição

• estabelecer o valor de present

present := S.i = x

Um primeiro refinamento para o algoritmo aparece na figura 2. No estado inicial é válido que

S.0 ≤ x < S.m

2Ao ler estas notas você estará lendo minha interpretação, o que certamente não deveria ser substituto para a leitura do original.

binsearch ( x , S . 0 . .m−1, present ) i s

{x ∈ Int ∧ m>0 ∧ S.0..m − 1 ∈ Int ∧ ordered.S(0..m − 1)}

[[ Var i ∈ Int

e s t a b l i s h R
; present := x=S . i

]]

Figura 2. Pesquisa binária: primeiro refinamento

binsearch ( x , S . 0 . .m−1, present ) i s

{x ∈ Int ∧ m>0 ∧ S.0..m − 1 ∈ Int ∧ ordered.S(0..m − 1)}

[[ Var i ∈ Int i n i t i a l l y i =1∧ j=m

do

B → shrink ( i , j ) od {P ∧ ¬B} {R}

present := x=S . i

]]

Figura 3. Pesquisa binária: segundo refinamento

Generalizando esta proposição produzimos a invariante

P =^{△} S.i ≤ x < S.j ∧ 0 ≤ i < j ≤ m

O objetivo do algoritmo consiste em reduzir a seção entre i e j até que j seja i + 1, quando a condição R estará estabelecida. A variável derivada será

T =^{△} j − i

e a guarda do loop

B =^{△} j ≠ i + 1

O novo algoritmo aparece na figura 3.

shrink ( i , j ) i s

[[ let h :− i <h< j

in i f

x < S . h → j :=h ][ S.h ≤ x → i :=h f i

tel ]]

Figura 4. Pesquisa binária: shrink

A seção entre i e j pode ser reduzida por:

• redução de j para algum h tal que

x < S.h ∧ i ≤ h < j

que mantém a validade da invariante;

• incremento de i para algum h tal que

S.h ≤ x ∧ i < h ≤ j

que também mantém a validade da invariante. Se existem duas ou mais alternativas para a mesma ação, considere

um comando alternativo: if. Veja o algoritmo de shrink na figura 4.

Duas construções importantes se destacam na especificação de shrink: o uso do comando alternativo e o uso da atribuição não determinística.

O comando alternativo da linguagem de comandos guardados de Dijkstra é não-determinístico. O trecho da figura 5 define que:

• ao menos um dos B_{i} (0 ≤ i ≤ n) deve ser verdadeiro;

• se mais de um B_{i} for válido, um deles será selecionado sendo a

ação S_{i} respectiva executada. Provar a correção desse comando demanda provar que:

(∃i : 0 ≤ i ≤ n : B_{i}) ∧ (∀i : 0 ≤ i ≤ n : Q ∧ B_{i} ⇒ P

A idéia de usar atribuições não determinísticas em especificações de programas parece ter sido proposta originialmente por Morris, ver por exemplo [10]. A semântica dessa atribuição é dada pelo seguinte cálculo de pré-condição:

wp(z :Q, P) = (∀x : Q.x ⇒ P)

Nessa especificação, o valor de h é escolhido arbitrariamente de um range válido. No entanto, se o valor de h for a média aritmética de i e j, cada ciclo do loop reduzirá a seção do array a considerar pela {Q} i f

B_{0} −> S_{0} ][B_{1} −> S_{1} … ][B_{n} −> S_{n}

f i {P }

Figura 5. Comando if

binsearch ( x , S . 0 . .m−1, present ) i s

{x ∈ Int ∧ m>0 ∧ S.0..m − 1 ∈ Int ∧ ordered.S(0..m − 1)} [[ Var i , j , h ∈ Int

i n i t i a l l y i =0∧ j=m do {P } j ≠ i +1 → {P ∧ B}

let h = ( i+j )/2
in {P ∧ B ∧ i < h < j}
i f x < S . h → j :=h
{P }
][ S . h≤x → i :=h
{P }
f i {P }

od

{P ∧ ¬B} {R}

present := x=S . i

]]

Figura 6. Pesquisa binária

metade: o que significa esgotar o array em O(lg m) passos. O algoritmo completo segue na figura 6.

Considere também nas provas a seguir que:

B_{0} =^{△} x < S.h

B_{1} =^{△} S.h ≤ x

16 RAUL H.C.LOPES

e que:

S_{0} =^{△} j := h

S_{1} =^{△} i := h

Assuma que

S.0 ≤ x < S.m ∧ S(m − 1) < S.m

e prove os teoremas a seguir. Os teoremas 19 e 18 consideram os casos em que x < S.0 e x > S.m − 1. Teorema 9. Prove que

P ∧ B ⇒ wp(h := (i + j)/2, i < h < j)

Prova. Trivial: aritmética.

□

O teorema a seguir estabelece que, em qualquer ciclo em que P e B são válidos, o comando alternativo não bloqueia. Teorema 10. Prove que

P ∧ B ⇒ (B_{0} ∨ B_{1})

Prova. Trivial. B_{0} é a negação de B_{1}: a implicação é sempre verdadeira.

□

Teorema 11. Prove que P vale inicialmente. Prova. Exercício.

□

Os teoremas a seguir provam que o comando alternativo preserva a invariante do loop. Teorema 12. Prove que

P ∧ B ∧ i < h < j ⊢ B_{0} ⇒ wp(S_{0}, P)

Prova.

P ∧ B ∧ i < h < j ⊢ B_{0} ⇒ wp(S_{0}, P) = ⟨Teorema da dedução⟩

⊢ P ∧ B ∧ i < h < j ∧ B_{0} ⇒ wp(S_{0}, P)

□

O objetivo, então, consistirá em provar que:

P ∧ B ∧ i < h < j ∧ B_{0} ⇒ wp(S_{0}, P)

wp(S,_{)} 0P

=

0 ≤ i < h ≤ m ∧ S.i ≤ x < S.h

=⟨lógica⟩

0 ≤ i ∧ i < h ∧ h ≤ m ∧ S.i ≤ x ∧ x < S.h

⇐⟨Eliminação de ∧⟩

0 ≤ i ∧ i < h ∧ h < j ∧ j ≤ m ∧ S.i ≤ x ∧ x < S.h = ⟨trasitividade de <⟩ 0 ≤ i < j ≤ m ∧ h < j ∧ S.i ≤ x ∧ x < S.h

⇐⟨h < j e S ordenado⟩

0 ≤ i < j < m ∧ S.i ≤ x ∧ x < S.j ∧ x < S.h ∧ i < h < j = P ∧ B ∧ i < h < j ∧ B_{0}

Teorema 13. Prove que

P ∧ B ∧ i < h < j ⊢ B_{1} ⇒ wp(S_{1}, P)

Prova. Parecido com anterior. Exercício.

□

Teorema 14. Prove que P é uma invariante do loop do algoritmo da figura 6. Prova. P vale inicialmente, de acordo com teorema 11, e validade de P é mantida pelo loop, segundo teorema 12.

□

Teorema 15. Prove que

P ∧ B ⇒ T > 0

Prova. Exercício.

□

18 RAUL H.C.LOPES

Teorema 16. Prove que T decresce, provando que

P ∧ B ∧ i < h < j ⊢ ∀v : T = v ⇒ wp(S_{0}, T > v)

e que

P ∧ B ∧ i < h < j ⊢ ∀v : T = v ⇒ wp(S_{0}, T > v)

Prova. Exercício.

□

Teorema 17. Prove que o loop do algoritmo da figura 6 termina. Prova. Exercício.

□

Teorema 18. Assuma que x > S(m − 1) e prove que P ainda é uma invariante do algoritmo da figura 6. Prova. Exercício.

□

Teorema 19. Assuma que x < S.0 e prove que

i = 0 ∧ x < S.0

é uma invariante do loop do algoritmo da figura 6. Prova. Exercício.

□

Teorema 20. Prove que o algoritmo estabelece

present = ∃k : 0 ≤ k < m : S.k = x

Prova. Exercício.

□

Algumas observações finais sobre o algoritmo de pesquisa binária:

• A derivação do algoritmo é feita passo a passo baseada em condições logicamente consistentes: o resultado inevitável é um algoritmo correto. Na verdade, a metodologia de programação de Dijkstra dá relevância a esse processo de derivação do algoritmo ficando a prova em segundo plano: por exemplo, [3] não contém a prova de correção do algoritmo final.

• A prova de correção levou em conta três: x ser menor do que S.0, x estar entre S.0 e S.m−1 e x ser maior do que S.m−1. É possível, no entanto, estabelecer um invariante mais forte para o mesmo algoritmo e construir provas para x genérico.

• Embora a prova assuma que S.m > S.m − 1, S.m não é usado no algoritmo. No segmentation fault!

3. Operações sobre os elementos de uma seqüência

3.1. Soma dos elementos de uma seqüência. Dada uma seqüência S de m inteiros, calcule a soma dos elementos de S.

O objetivo consiste em estabelecer:

R = ∑_{0≤i<m} S.i

3.1.1. Solução funcional. A solução funcional é dada a partir da derivação a seguir:

R = ∑i : 0 ≤ i < m : S.i
= (S.0 + S.1 + ⋯ + S.m − 1)
= ((⋯ ((S.0 + S.1) + S.2) + ⋯) + S.m − 1)
= ((⋯ ((0 + S.0) + S.1) + S.2) + ⋯) + S.m − 1)

Que é calculado em Haskell, assumindo que S é agora representado por uma lista, por

f o l d l ( + ) 0 S

As definições de foldl e foldr são padrão:

f o l d l op z [ ] = z
f o l d l op a ( x : xs ) = f o l d l op ( op a x ) xs
foldr op z [ ] = z
foldr op a ( x : xs ) = op x ( foldr op a xs )

Note que a associatividade à esquerda da operação (+), implementada através do combinador foldl, pode ser reescrita como associatividade à direita, implementada via foldr.

foldr ( + ) 0 S

que, por sua vez, implementa o padrão recursivo da função seqsum a seguir:

seqsum [ ] = 0
seqsum ( x : xs ) = x+(seqsum xs )

Nessa função fique mais explícita a complexidade da soma dos elementos de uma seqüência. Ela demanda:

• m operações de soma;

• m + 1 testes de fim de seqüência;

• m operações para recuperar o tail da seqüência atual.

3.1.2. Solução imperativa. A solução imperativa é facilmente obtida de uma generalização do output desejado

R = ∑_{0≤i<m} S.i

a partir da obervação da associatividade à esquerda do operador (+):

• ao final do loop, m elementos terão sido acumulados produzindo o resultado desejado;

• em um ciclo k, os k primeiros elementos terão sido acumulados. Na fórmula de R, generaliza-se a constante M para uma variável k e insere-se a restrição de que k deve ser maior ou igual a zero elementos e menor ou igual a m elementos.

P = (0 ≤ k ≤ m ∧ R = ∑_{0≤i<k} S.i)

P será obviamente a invariante do loop. A guarda do loop deve indicar quantos elementos ainda devem ser acumulados:

B = (m − k ≠ 0)

o que dá a variável derivada

T = m − k

e a condição inicial

initially k = 0 ∧ R = 0

A derivação final do programa consiste apenas em definir o comando guardado do loop que decrescerá T, mantendo a invariante P. Fica como exercício.

Exercício 3. Prove que o programa derivado para cálculo da soma dos elementos de uma seqüência está correto.

(1) Prove que ele está parcialmente correto.

(a) Prove que P é válido inicialmente.

(b) Prove que P ∧B .⇒ wp(α, P), onde α é o comando guardado do loop.

(c) Prove que P ∧ ¬B .⇒ R

(2) Prove que o loop termina. (a) Prove que P ∧ B .⇒. T > 0

(b) Prove que eventualmente T = 0 e o loop termina.

4. Permutações de uma seqüência

Esta seção aborda algoritmos que tratam de variantes de um mesmo problema: calcular alguma permutação de uma seqüência.

4.1. Section swap. Dado um vetor S.0..m − 1 ∈ Int e dados inteiros p, q, n tais que

n > 0 ∧ 0 ≤ p ≤ p + n ≤ q ≤ q + n ≤ m

calcule in-locu uma permutação de S que consiste em trocar as seções^{3} S.p..p + n − 1 por S.q..q + n − 1. Assumindo que B contém o valor inicial de S, o objetivo consiste em calcular ^{4}

R =^{△}S(0..p − 1) = B(0..p − 1)
∧ S(p..p + n − 1) = B(q..q + n − 1)
∧ S(p + n..q − 1) = B(p + n..q − 1)
∧ S(q..q + n − 1) = B(p..p + n − 1)
∧ S(q + n..m − 1) = B(q + n..m − 1)

A invariante P do algoritmo para calcular o swap de seções do array é obtida generalizando R, via troca da constante n por uma variável k, que deve ser um inteiro entre 0 e n.

P =^{△}0 ≤ k ≤ n
∧ S(0..p − 1) = B(0..p − 1)
∧ S(p..p + k − 1) = B(q..q + k − 1)
∧ S(p + k..q − 1) = B(p + k..q − 1)
∧ S(q..q + k − 1) = B(p..p + k − 1)
∧ S(q + k..m − 1) = B(q + k..m − 1)

3Exemplo retirado de [7].

4B é introduzido aqui como variável auxiliar para facilitar a derivação do programa.

swapsec (S , p , q , n ) i s

{m ≥ 0 ∧ S.0..m − 1 ∈ Int ∧ p, q, n ∈ Int ∧ n ≥ 0} [[ Var k ∈ Int ;

i n i t i a l l y B=S ∧ i =0 ∧ m≥0 ∧ n≥0 {P } do

n−k>0 → S(p+k ) , S( q+k ) , k:=S( q+k ) , S(p+k ) , k+1 od {P ∧ ¬B}

{R} ]]

Figura 7. Array swap

A variável derivada é óbvia:

T =^{△} n − k

Essa variável deve decrescer até atingir o valor 0, o que dá a guarda do loop:

B_{0} =^{△} n − k > 0

Progresso no loop demanda reduzir T, via incremento de k, mantendo a validade de P. A ação associada à transição do loop, facilmente derivada a partir de P e do incremento de k, fica:

S_{0} =^{△} S(p + k), S(q + k), k := S(q + k), S(p + k), k + 1

O algoritmo está na figura 7.

Exercício 4. Prove que:

(1) P ∧ ⇒ T > 0

(2) O programa da figura 7 termina.

(3) P ∧ B_{0} ⇒ wp(S_{0}, P)

(4) P é uma invariante do loop do programa da figura 7.

(5) P ∧ ¬B_{0} ⇒ R

O primeiro objetivo consiste em provar que a execução de S_{0} não altera a validade de P. Antes, porém, observe que, após S_{0}, um novo S, que chamaremos de S′, é produzido.

S′ =^{△} (S; p + k : S(q + k); q + k : S(p + k))

Note que S′ define os novos valores de S e que as igualdades abaixo de verificam e elas mostram que seções de S não foram alteradas.

S′(0..p + k − 1) = S(0..p + k − 1)
S′(p + k + 1..q + k − 1) = S(p + k + 1..q + k − 1)
S′(q + k + 1..m − 1) = S(q + k + 1..m − 1)

As igualdades a seguir mostram as posições de S que foram alteradas:

S′(p + k) = S(q + k)
S′(q + k) = S(p + k)

Além disso, observe que

S(q + k) = B(q + k)
S(p + k) = B(p + k)

Esses dois conjutnos de igualdades são usadas na prova do teorema a seguir.

Teorema 21. P ∧ B_{0} ⇒ wp(S_{0}, P)

Prova. wp(S_{0}, P)

=−1 ≤ k ≤ n − 1

∧ S′(0..p − 1) = B(0..p − 1) ∧ S′(p..p + k) = B(q..q + k) ∧ S′(p + k + 1..q − 1) = B(p + k + 1..q − 1) ∧ S′(q..q + k) = B(p..p + k) ∧ S′(q + k + 1..m − 1) = B(q + k + 1..m − 1)

=−1 ≤ k ≤ n ∧ k ≠ n

∧ S′(0..p − 1) = B(0..p − 1) ∧ S′(p..p + k − 1) = B(q..q + k − 1) ∧ S′(p + k) = B(q + k) ∧ S′(p + k..q − 1) = B(p + k..q − 1) ∧ S′(q..q + k − 1) = B(p..p + k − 1) ∧ S′(q + k) = B(p + k) ∧ S′(q + k..m − 1) = B(q + k..m − 1)

=⟨subsituindo S′⟩

−1 ≤ k ≤ n ∧ k ≠ n ∧ S(0..p − 1) = B(0..p − 1) ∧ S(p..p + k − 1) = B(q..q + k − 1) ∧ B(q + k) = B(q + k) ∧ S(p + k..q − 1) = B(p + k..q − 1) ∧ S(q..q + k − 1) = B(p..p + k − 1) ∧ B(p + k) = B(p + k) ∧ S(q + k..m − 1) = B(q + k..m − 1)

=⟨simplificando ∧⟩

−1 ≤ k ≤ n ∧ k ≠ n ∧ S(0..p − 1) = B(0..p − 1) ∧ S(p..p + k − 1) = B(q..q + k − 1) ∧ S(p + k..q − 1) = B(p + k..q − 1) ∧ S(q..q + k − 1) = B(p..p + k − 1) ∧ S(q + k..m − 1) = B(q + k..m − 1)

⇐⟨Introdução de ∨⟩

0 ≤ k ≤ n ∧ k ≠ n ∧ S(0..p − 1) = B(0..p − 1) ∧ S(p..p + k − 1) = B(q..q + k − 1) ∧ S(p + k..q − 1) = B(p + k..q − 1) ∧ S(q..q + k − 1) = B(p..p + k − 1) ∧ S(q + k..m − 1) = B(q + k..m − 1)

=P ∧ B_{0}

□

4.2. Reverso de uma seqüência. Dada uma seqüência, cálcule seu reverso.

4.2.1. A lógica. Gries e Schneider, em [7], apresentam o seguinte conjunto de axiomas para definir o reverso de um seqüência.

rev.ε = ε (6) rev.c ◁ x = (rev.x)ˆ ˆ ˆ ˆ ˆˆ ⟨c⟩ (7)

onde

εˆ ˆ ˆ ˆ ˆˆ y = y (8) (c ◁ x)ˆ ˆ ˆ ˆ ˆˆ y = c ◁ (xˆ ˆ ˆ ˆ ˆˆ y) (9)

A solução funcional, em Haskell, para o problema do reverso de uma seqüência é retirada diretamente dos axiomas 6 e 7.

Talvez seja mais intuitivo ler y como reverso de x quando: o último elemento de x é o primeiro de y, o primeiro de x é último em y e a subseqüência que vai do segundo ao penúltimo elemento de y é o reverso da subseqüência que vai do segundo ao penúltimo elemento de x.

reverse.⟨⟩ = ⟨⟩ (10) reverse.⟨a⟩ = ⟨a⟩ (11) reverse(a ◁ x ▷ b) = b ◁ (reverse.x) ▷ a (12) onde (13) x ▷ a = xˆ ˆ ˆ ˆ ˆˆ ⟨a⟩ (14)

Exercício 5. Prove que:

(1) rev.⟨a⟩ˆ ˆ ˆ ˆ ˆˆ x = (rev.x)ˆ ˆ ˆ ˆ ˆˆ ⟨a⟩

(2) rev.xˆ ˆ ˆ ˆ ˆˆ ⟨a⟩ = ⟨a⟩ˆ ˆ ˆ ˆ ˆˆ rev.x

(3) rev.⟨a⟩ˆ ˆ ˆ ˆ ˆˆ xˆ ˆ ˆ ˆ ˆˆ ⟨b⟩ = ⟨b⟩ˆ ˆ ˆ ˆ ˆˆ (rev.x)ˆ ˆ ˆ ˆ ˆˆ ⟨a⟩

A última prova do exercício 5 fornece a base a definição da invariante do algoritmo imperativo para cálculo do reverso de uma seqüência.

4.2.2. Solução imperativa. A invariante do algoritmo para cálculo do reverso de uma seqüência surge da generalização do teorema provado no exercício 5.

Exercício 6. Prove que (1) rev.xˆ ˆ ˆ ˆ ˆˆ y = (rev.y)ˆ ˆ ˆ ˆ ˆˆ (rev.x)

(2) rev.xˆ ˆ ˆ ˆ ˆˆ wˆ ˆ ˆ ˆ ˆˆ yˆ ˆ ˆ ˆ ˆˆ z = (rev.z)ˆ ˆ ˆ ˆ ˆˆ (rev.wˆ ˆ ˆ ˆ ˆˆ y)ˆ ˆ ˆ ˆ ˆˆ (rev.x)

Uma definição alternativa para o reverso de uma seqüência, mais adequada ao tratamento de seqüência como array.

Definição 3.

S.i..j = rev(B.h..k) =^{△}
j − i = k − h
∧. j − i < 0
∨ (S.i = B.k ∧ S(i + 1..j) = rev(B.h..k − 1))

Exercício 7. Apresente uma definição formal de seção de uma seqüência S e prove que para qualquer inteiro i não negativo, menor do que zero:

rev.S(0..m − 1) =(rev.S(m − 1 − i..m − 1))
ˆ̂ˆ̂ˆˆ (rev.S(i..m − 1 − i))
ˆ̂ˆ̂ˆˆ (rev.S(0..i))

Exercício 8. Apresente uma algoritmo para cálculo do reverso de uma seqüênciae prove sua correção.

(1) Use teorema provado no exercício 7 para definir a invariante P do loop do algoritmo.

(2) Use a variável derivada

T = m − 2i

(3) Define a guarda B do loop e prove que

P ∧ B .⇒. wp(α, P)

onde α é

S.i, S(m − 1 − i), i := S(m − 1 − i), S.i, i + 1

4.3. Partição de uma seqüência por um pivô. Dada uma seqüência S e um elemento indicado como pivô, o objetivo consiste em particionar a seqüência original em duas seqüências: de elementos menores do que o pivôe de elementos maiores do que o pivô. Acrescentada a restrição de que a seqüência seja representada em um vetor e de que as trocas devem ser realizadas no próprio vetor, evitando a geração de seqüências adicionais, o objetivo consiste em encontrar uma permutação de S e um valor p tal que

R =^{△} 0 ≤ p < m ∧ S(0..p − 1) < S.p ∧ S(p + 1..m − 1) ≥ S.p

Dado que a posição inicial do pivô não foi especificada, é razoável arbitrar que ele se encontrar inicialmente na posição 0 e que o objetivo consiste em estabelecer para algum i que

S(0..i − 1) < S.0 ∧ S(i..m − 1) > S.0

para, então, computar R.

Note que se

β =^{△} S.0, S(i − 1), p := S(i − 1), S.0, i − 1

então

wp(β, R)

=0 ≤ i − 1 < m
∧S′(0..i − 1 − 1) < S′(i − 1)
∧S′(i − 1 + 1..m − 1) ≥ S′(i − 1)
∧i − 1 = i − 1
=⟨aritmética⟩
∧1 ≤ i ≤ m
∧S′(0..i − 1 − 1) < S′(i − 1)
∧S′(i − 1 + 1..m − 1) ≥ S′(i − 1)
=1 ≤ i ≤ m
∧S′(0) < S′(i − 1) ∧ S′(1..i − 2) < S′(i − 1)
∧S′(i..m − 1) ≥ S′(i − 1)
=⟨S′ = (S; 0 : S(i − 1); i − 1 : S.0)⟩

1 ≤ i ≤ m

∧S(i − 1) < S(0) ∧ S(1..i − 2) < S(0)
∧S(i..m − 1) ≥ S(0)
=1 ≤ i ≤ m
∧S(1..i − 1) < S(0)
∧S(i..m − 1) ≥ S(0)

A precondição derivada define o estado final do loop que construirá a permutação de S:

1 ≤ i ≤ m ∧ S(1..i − 1) < S(0) ∧ S(i..m − 1) ≥ S(0)

Uma generalização dessa condição, substituindo m por j, define parte da invariante:

1 ≤ i ≤ m ∧ S(1..i − 1) < S(0) ∧ S(i..j − 1) ≥ S(0)

A invariante é obtida fortalecendo essa condição pela observação de que:

• os elementos menores do que S(0) estão compreendidos entre as posições 1 e i − 1;

• os elementos restantes estão compreendidos entre as posições i e j − 1, o que impõem i ≤ j, devendo j ficar limitado a m.

P =^{△} 1 < i ≤ j ≤ m ∧ S(1..i − 1) < S(0) ∧ S(i..j − 1) ≥ S(0)

O loop analisará cada elemento da seqüência da esquerda para direita, incorporando a cada transição um elemento uma das partições e reduzindo a secão não particionada do vetor, que é S(j..m − 1). Sua variável derivada

T =^{△} (m − j)

A guarda do loop

B =^{△} m − j ≠ 0

O progresso do loop em direção ao ponto-fixo demanda decrementar T, que só pode ocorrer incrementando j, já que m é constante. Por outro lado, o incremento de j pode ocorrer sem perturbar invariante em duas condições:

• quando B_{0} é válido

B_{0} =^{△} S.j ≥ S.0

;

• quando B_{1} é válido

B_{1} =^{△} S.j < S.0

que demanda incorporar S.j à partição dos elementos menores do que S.0.

A figura 8 apresenta o algoritmo, que Bentley em [1] atribui a Lomut. Note que cada comando apresenta suas precondições e poscondições detalhadas. As ações S_{0} e S_{1} são facilmente derivadas:

S_{0} =^{△} j := j + 1

S_{1} =^{△} S.i, S.j, i, j := S.j, S.i, i + 1, j + 1

O próximo passo consiste em provar a correção do algoritmo da figura 8.

Teorema 22. Prove que P é válida inicialmente. Prova.

p a r t i t i o n (S , p ) i s

{S.0..m − 1 ∈ Int ∧ m ≥ 0}

[[ Var B . 0 . .m−1 ∈ Int ; i , j ∈ Int ; i n i t i a l l y B=S ∧ i =1 ∧ j=1 do {P }

B → i f {P ∧ B}

B_{0} → {P ∧ B ∧ B_{0}}S_{0}{P }
][ B_{1} → {P ∧ B ∧ B_{1}}S_{1}{P }
f i {P }

od

{P ∧ ¬B}

S . 0 , S( i −1),p:=S( i −1),S . 0 , i−1

{R} ]]

Figura 8. Partição de Lomut

Exercício.

□

O teorema a seguir estabelece que quando a invariante P e a guarda do loop são válidas, ao menos uma das guardas do comando if será válida.

Teorema 23. Prove que

P ∧ B ⇒ (B_{0} ∨ B_{1})

Prova. B_{0} e B_{1} são complementares.

□

O próximo teorema estabelece que a ação associada à primeira guarda do if, ação S_{0} da guarda B_{0}, preserva a invariante do loop. Sua prova fica como exercício.

Teorema 24. Provar que

P ∧ B ∧ B_{0} ⇒ wp(S,_{)} 0P)

Prova. Exercício: veja prova a seguir.

□

30 RAUL H.C.LOPES

O teorema a seguir estabelece que a ação S_{1}, associada à guarda B_{1} preserva a invariante P. Teorema 25. Provar que

P ∧ B ∧ B_{1} ⇒ wp(S,_{)} 1P)

Prova.

wp(S_{1}, P) = 1 < i + 1 ≤ j + 1 ≤ m ∧ S′(1..i + 1 − 1) < S′(0) ∧ S′(i + 1..j + 1 − 1) ≥ S′(0)

= − 1 < i ≤ j ≤ m − 1 ∧ S′(1..i) < S′(0) ∧ S′(i + 1..j) ≥ S′(0)

= 0 ≤ i ≤ j ≤ m ∧ (m ≠ j ∧ S′(1..i) < S′(0) ∧ S′(i + 1..j) ≥ S′(0))

= (0 = i ∨ 0 < i ≤ j ≤ m) ∧ (m ≠ j ∧ S′(1..i) < S(0) ∧ S′(i + 1..j) ≥ S′(0))

= (0 = i ∧ m ≠ j ∧ S′(1..i) < S′(0) ∧ S′(i + 1..j) ≥ S′(0)) ∨ (0 < i ≤ j ≤ m ∧ m ≠ j

∧ S′(1..i) < S′(0) ∧ S′(i + 1..j) ≥ S′(0))

⇐⟨Introdução de lor⟩

(0 < i ≤ j ≤ m) ∧ m ≠ j ∧ S′(1..i) < S′(0) ∧ S′(i + 1..j) ≥ S′(0)

=⟨trocando S′ por seus valores em S⟩

(0 < i ≤ j ≤ m) ∧ m ≠ j ∧ S(1..i − 1) < S(0) ∧ S(i) ≥ S(0) ∧ S(i + 1..j − 1) ≥ S(0) ∧ S(j) < S(0)

= P ∧ B ∧ B_{1}

□

Estas notas foram completamente alteradas desde a última versão, mas ainda necessitam muitas correções e adições em termos de:

• novos exemplos;

• novos exercícios;

• explanação da semântica da linguagem usada;

• substituição total dos exemplos em Haskell por equivalentes em lógica;

• validações usando algum assistente de prova. Agradeço contribuições nesse sentido.

Referências

1. Jon Bentley, Programming pearls, second ed., Addison-Wesley, 2000.

2. Haskell B. Curry, Foundations of mathematical logic, Dover Publications, Inc., 1977.

3. Edsger W. Dijkstra, Fillers at the YoP intitute, in Formal Development of Programs and Proofs [4].

4. Edsger W. Dijkstra (ed.), Formal development of programs and proofs, Addison-Wesley, 1990.

5. E.W. Dijkstra, A discipline of programming, Prentice Hall, Englewood Cliffs, 1976.

6. David Gries, The science of programming, Springer-Verlag, New York, 1981.

7. , The science of programming, Springer-Verlag, 1985.

8. David Gries and Fred B.Schneider, A logical approach to discrete mathematics, Springer-Verlag, 1993.

9. Donald E. Knuth, The art of computer programmimg, volume 3: Sorting and searching, Addison-Wesley, 1998.

10. Joseph M. Morris, Piecewise data refinment, in Dijkstra [4].