Uma prova é muito mais difícil no mundo da OOP por causa de efeitos colaterais, herança irrestrita e nullser membro de todos os tipos. A maioria das provas se baseia em um princípio de indução para mostrar que você cobriu todas as possibilidades, e todas essas três coisas tornam isso mais difícil de provar.
Digamos que estamos implementando árvores binárias que contêm valores inteiros (para manter a sintaxe mais simples, não trarei programação genérica para isso, embora isso não mude nada.) No ML padrão, eu definiria isso como esta:
datatype tree = Empty | Node of (tree * int * tree)
Isso introduz um novo tipo chamado treecujos valores podem vir exatamente em duas variedades (ou classes, que não devem ser confundidas com o conceito OOP de uma classe) - um Emptyvalor que não carrega informações e Nodevalores que carregam uma tripla cuja primeira e última elementos são se treecujo elemento do meio é um int. A aproximação mais próxima a esta declaração no OOP seria assim:
public class Tree {
private Tree() {} // Prevent external subclassing
public static final class Empty extends Tree {}
public static final class Node extends Tree {
public final Tree leftChild;
public final int value;
public final Tree rightChild;
public Node(Tree leftChild, int value, Tree rightChild) {
this.leftChild = leftChild;
this.value = value;
this.rightChild = rightChild;
}
}
}
Com a ressalva de que variáveis do tipo Árvore nunca podem ser null.
Agora vamos escrever uma função para calcular a altura (ou profundidade) da árvore e supor que temos acesso a uma maxfunção que retorna o maior de dois números:
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
Nós definimos a heightfunção de casos - não há uma definição para Emptyárvores e uma definição para Nodeárvores. O compilador sabe quantas classes de árvores existem e emitirá um aviso se você não definir os dois casos. A expressão Node (leftChild, value, rightChild)na assinatura da função liga os valores da 3-tupla às variáveis leftChild, valuee, rightChildrespectivamente, para que possa consultá-las na definição da função. É semelhante a ter declarado variáveis locais como este em uma linguagem OOP:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
Como podemos provar que implementamos heightcorretamente? Podemos usar a indução estrutural , que consiste em: 1. Prove que heightestá correto no (s) caso (s) base (s) do nosso treetipo ( Empty) 2. Supondo que as chamadas recursivas heightestejam corretas, prove que está correto no (s) caso (s) heightnão básico (s) ) (quando a árvore é realmente a Node).
Para a etapa 1, podemos ver que a função sempre retorna 0 quando o argumento é uma Emptyárvore. Isso está correto por definição da altura de uma árvore.
Para a etapa 2, a função retorna 1 + max( height(leftChild), height(rightChild) ). Supondo que as chamadas recursivas realmente retornem a altura das crianças, podemos ver que isso também está correto.
E isso completa a prova. Os passos 1 e 2 combinados esgotam todas as possibilidades. Observe, no entanto, que não temos mutação, nulos e existem exatamente duas variedades de árvores. Tire essas três condições e a prova rapidamente se torna mais complicada, se não impraticável.
EDIT: Como essa resposta chegou ao topo, gostaria de adicionar um exemplo menos trivial de uma prova e cobrir a indução estrutural um pouco mais completamente. Acima, provamos que, se heightretorna , seu valor de retorno está correto. Porém, não provamos que ele sempre retorna um valor. Também podemos usar a indução estrutural para provar isso (ou qualquer outra propriedade). Novamente, durante a etapa 2, podemos assumir que a propriedade retém as chamadas recursivas, desde que todas as chamadas recursivas operem em um filho direto do árvore.
Uma função pode falhar ao retornar um valor em duas situações: se lança uma exceção e se faz um loop para sempre. Primeiro, vamos provar que, se nenhuma exceção for lançada, a função será encerrada:
Prove que (se nenhuma exceção for lançada) a função será encerrada para os casos base ( Empty). Como retornamos 0 incondicionalmente, ele termina.
Prove que a função termina nos casos não básicos ( Node). Há três chamadas de função aqui: +, max, e height. Sabemos disso +e maxterminamos porque fazem parte da biblioteca padrão da linguagem e são definidos dessa maneira. Como mencionado anteriormente, podemos assumir que a propriedade que estamos tentando provar é verdadeira em chamadas recursivas, desde que operem em subárvores imediatas; portanto, as chamadas heighttambém serão encerradas.
Isso conclui a prova. Observe que você não poderá provar a rescisão com um teste de unidade. Agora tudo o que resta é mostrar que heightnão gera exceções.
- Prove que
heightnão lança exceções no caso base ( Empty). Retornar 0 não pode gerar uma exceção, por isso terminamos.
- Prove que
heightnão gera exceção no caso não básico ( Node). Suponha mais uma vez que sabemos +e maxnão lançamos exceções. E a indução estrutural nos permite assumir que as chamadas recursivas também não serão lançadas (porque operam nos filhos imediatos da árvore). Mas espere! Esta função é recursiva, mas não recursiva de cauda . Nós poderíamos explodir a pilha! Nossa tentativa de prova descobriu um bug. Podemos corrigi-lo mudando heightpara ser recursivo da cauda .
Espero que isso mostre que as provas não precisam ser assustadoras ou complicadas. De fato, sempre que você escreve código, constrói informalmente uma prova em sua mente (caso contrário, não se convenceria de que acabou de implementar a função.) Ao evitar mutações nulas, desnecessárias e herança irrestrita, você pode provar que sua intuição é corrija com bastante facilidade. Essas restrições não são tão severas quanto você imagina:
null é uma falha de linguagem e acabar com ela é incondicionalmente boa.
- Às vezes, a mutação é inevitável e necessária, mas é necessária com muito menos frequência do que você imagina - especialmente quando você tem estruturas de dados persistentes.
- Quanto a ter um número finito de classes (no sentido funcional) / subclasses (no sentido OOP) versus um número ilimitado delas, esse é um assunto grande demais para uma única resposta . Basta dizer que existe uma troca de design por aí - probabilidade de correção versus flexibilidade de extensão.