Una prova è molto più dura nel mondo OOP a causa di effetti collaterali, eredità illimitata ed nullessere un membro di ogni tipo. La maggior parte delle prove si basa su un principio di induzione per dimostrare che hai coperto tutte le possibilità e tutte e 3 queste cose rendono più difficile dimostrarlo.
Diciamo che stiamo implementando alberi binari che contengono valori interi (per mantenere la sintassi più semplice, non inserirò la programmazione generica in questo, sebbene non cambierebbe nulla.) In Standard ML, definirei che Questo:
datatype tree = Empty | Node of (tree * int * tree)
Questo introduce un nuovo tipo chiamato i treecui valori possono venire esattamente in due varietà (o classi, da non confondere con il concetto OOP di una classe) - un Emptyvalore che non porta informazioni e Nodevalori che portano una 3-tupla il cui primo e ultimo gli elementi sono se treeil cui elemento centrale è un int. L'approssimazione più vicina a questa dichiarazione in OOP sarebbe simile a questa:
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;
}
}
}
Con l'avvertenza che le variabili di tipo Albero non possono mai essere null.
Ora scriviamo una funzione per calcolare l'altezza (o profondità) dell'albero e supponiamo di avere accesso a una maxfunzione che restituisce il più grande di due numeri:
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
Abbiamo definito la heightfunzione in base ai casi: esiste una definizione per Emptyalberi e una definizione per Nodealberi. Il compilatore sa quante classi di alberi esistono e genererebbe un avviso se non si definissero entrambi i casi. L'espressione Node (leftChild, value, rightChild)nella firma funzione associa i valori della 3-tupla alle variabili leftChild, valuee rightChildrispettivamente in modo da poter fare riferimento ad essi nella definizione della funzione. È simile ad aver dichiarato variabili locali come questa in un linguaggio OOP:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
Come possiamo dimostrare di aver implementato heightcorrettamente? Possiamo usare l' induzione strutturale , che consiste in: 1. Dimostrare che heightè corretto nei casi di base del nostro treetipo ( Empty) 2. Supponendo che le chiamate ricorsive heightsiano corrette, dimostrare che heightè corretto per i casi non di base ) (quando l'albero è in realtà a Node).
Per il passaggio 1, possiamo vedere che la funzione restituisce sempre 0 quando l'argomento è un Emptyalbero. Questo è corretto per definizione dell'altezza di un albero.
Per il passaggio 2, la funzione ritorna 1 + max( height(leftChild), height(rightChild) ). Supponendo che le chiamate ricorsive restituiscano veramente l'altezza dei bambini, possiamo vedere che anche questo è corretto.
E questo completa la prova. I passaggi 1 e 2 combinati esauriscono tutte le possibilità. Si noti, tuttavia, che non abbiamo mutazione, niente null e ci sono esattamente due varietà di alberi. Elimina queste tre condizioni e la prova diventa rapidamente più complicata, se non impraticabile.
EDIT: Dal momento che questa risposta è salita in cima, vorrei aggiungere un esempio meno banale di una prova e coprire l'induzione strutturale un po 'più a fondo. Sopra abbiamo dimostrato che se heightrestituisce , il suo valore di ritorno è corretto. Tuttavia, non abbiamo dimostrato che restituisca sempre un valore. Possiamo usare anche l'induzione strutturale per dimostrarlo (o qualsiasi altra proprietà). Ancora una volta, durante il passaggio 2, possiamo assumere le proprietà trattenute delle chiamate ricorsive fintanto che tutte le chiamate ricorsive operano su un figlio diretto del albero.
Una funzione può non riuscire a restituire un valore in due situazioni: se genera un'eccezione e se viene ripetuta per sempre. Innanzitutto proviamo che se non vengono generate eccezioni, la funzione termina:
Dimostra che (se non vengono generate eccezioni) la funzione termina per i casi base ( Empty). Poiché restituiamo incondizionatamente 0, termina.
Prova che la funzione termina nei casi non di base ( Node). Ci sono tre chiamate di funzione qui: +, max, e height. Lo sappiamo +e maxterminiamo perché fanno parte della libreria standard del linguaggio e sono definiti in questo modo. Come accennato in precedenza, possiamo ammettere che la proprietà che stiamo cercando di dimostrare sia vera per le chiamate ricorsive fintanto che operano su sottotitoli immediati, quindi anche le chiamate per heightterminare.
Questo conclude la prova. Nota che non saresti in grado di provare la conclusione con un unit test. Ora non resta che mostrare che heightnon generano eccezioni.
- Dimostrare che
heightnon genera eccezioni nel caso di base ( Empty). Restituire 0 non può generare un'eccezione, quindi abbiamo finito.
- Dimostrare che
heightnon genera eccezioni sul caso non base ( Node). Supponi ancora una volta che sappiamo +e maxnon facciamo eccezioni. E l'induzione strutturale ci consente di presumere che le chiamate ricorsive non verranno lanciate (perché operano sui figli immediati dell'albero). Ma aspetta! Questa funzione è ricorsiva, ma non ricorsiva alla coda . Potremmo far saltare lo stack! La nostra tentata prova ha scoperto un bug. Possiamo risolverlo cambiando heightin modo ricorsivo della coda .
Spero che ciò dimostri che le prove non devono essere spaventose o complicate. In effetti, ogni volta che scrivi un codice, hai costruito in modo informale una prova nella tua testa (altrimenti, non saresti convinto di aver appena implementato la funzione.) Evitando la mutazione nulla, non necessaria e l'eredità senza restrizioni puoi provare che il tuo intuito è correggo abbastanza facilmente. Queste restrizioni non sono così dure come potresti pensare:
null è un difetto del linguaggio e eliminarlo è incondizionatamente buono.
- La mutazione è talvolta inevitabile e necessaria, ma è necessaria molto meno spesso di quanto si pensi, soprattutto quando si hanno strutture di dati persistenti.
- Per quanto riguarda avere un numero finito di classi (in senso funzionale) / sottoclassi (in senso OOP) contro un numero illimitato di esse, questo è un argomento troppo grande per una singola risposta . Basti dire che c'è un trade design là fuori: provabilità di correttezza vs flessibilità di estensione.