Ein Beweis ist in der OOP-Welt aufgrund von Nebenwirkungen, uneingeschränkter Vererbung und nullder Zugehörigkeit zu jedem Typ viel schwieriger . Die meisten Beweise beruhen auf einem Induktionsprinzip, um zu zeigen, dass Sie alle Möglichkeiten abgedeckt haben, und alle drei dieser Dinge erschweren den Beweis.
Nehmen wir an, wir implementieren Binärbäume, die ganzzahlige Werte enthalten (um die Syntax einfacher zu halten, werde ich keine generische Programmierung einbringen, obwohl dies nichts ändern würde.) In Standard ML würde ich das wie folgt definieren Dies:
datatype tree = Empty | Node of (tree * int * tree)
Dies führt einen neuen Typ namens ein, treedessen Werte in genau zwei Varianten (oder Klassen, nicht zu verwechseln mit dem OOP-Konzept einer Klasse) vorliegen können - ein EmptyWert, der keine Informationen enthält, und NodeWerte, die ein 3-Tupel enthalten, dessen erster und letzter Elemente sind trees und deren mittleres Element ist ein int. Die nächste Annäherung an diese Erklärung in OOP würde folgendermaßen aussehen:
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;
}
}
}
Mit der Einschränkung, dass Variablen vom Typ Tree niemals sein können null.
Schreiben wir nun eine Funktion zur Berechnung der Höhe (oder Tiefe) des Baums und nehmen an, dass wir Zugriff auf eine maxFunktion haben, die die größere von zwei Zahlen zurückgibt:
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
Wir haben die heightFunktion nach Fällen definiert - es gibt eine Definition für EmptyBäume und eine Definition für NodeBäume. Der Compiler weiß, wie viele Baumklassen vorhanden sind, und gibt eine Warnung aus, wenn Sie nicht beide Fälle definiert haben. Der Ausdruck Node (leftChild, value, rightChild)in der Funktion Unterschrift bindet die Werte des 3-Tupels zu den Variablen leftChild, valueund rightChildjeweils so dass wir sie in der Funktionsdefinition beziehen. Es ist vergleichbar damit, lokale Variablen wie diese in einer OOP-Sprache deklariert zu haben:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
Wie können wir nachweisen, dass wir heightrichtig implementiert haben? Wir können eine strukturelle Induktion verwenden , die aus Folgendem besteht: 1. Beweisen Sie, dass die Basisfälle heightunseres treeTyps ( Empty) heightkorrekt sind ( ). 2. Unter der Annahme, dass rekursive Aufrufe korrekt sind, beweisen Sie, dass sie heightfür die Nicht-Basisfälle korrekt sind ) (wenn der Baum tatsächlich a ist Node).
In Schritt 1 können wir sehen, dass die Funktion immer 0 zurückgibt, wenn das Argument ein EmptyBaum ist. Dies ist per Definition der Höhe eines Baumes korrekt.
Für Schritt 2 kehrt die Funktion zurück 1 + max( height(leftChild), height(rightChild) ). Angenommen, die rekursiven Aufrufe geben tatsächlich die Größe der Kinder zurück, können wir sehen, dass dies auch richtig ist.
Und das vervollständigt den Beweis. Die Schritte 1 und 2 zusammen schöpfen alle Möglichkeiten aus. Beachten Sie jedoch, dass wir keine Mutation, keine Nullen und genau zwei Baumarten haben. Nehmen Sie diese drei Bedingungen weg und der Beweis wird schnell komplizierter, wenn nicht unpraktisch.
EDIT: Da diese Antwort ganz oben angekommen ist, möchte ich ein weniger triviales Beispiel für einen Beweis hinzufügen und die strukturelle Induktion etwas gründlicher behandeln. Oben haben wir bewiesen, dass bei heightRückgabe der Rückgabewert korrekt ist. Wir haben jedoch nicht bewiesen, dass es immer einen Wert zurückgibt. Wir können die strukturelle Induktion auch verwenden, um dies (oder eine andere Eigenschaft) zu beweisen. Auch in Schritt 2 dürfen wir davon ausgehen, dass die Eigenschaften der rekursiven Aufrufe gültig sind, solange die rekursiven Aufrufe alle auf ein direktes untergeordnetes Element der Baum.
Eine Funktion kann in zwei Situationen keinen Wert zurückgeben: wenn sie eine Ausnahme auslöst und wenn sie für immer wiederholt wird. Lassen Sie uns zunächst beweisen, dass die Funktion beendet wird, wenn keine Ausnahmen ausgelöst werden:
Beweisen Sie, dass (wenn keine Ausnahmen ausgelöst werden) die Funktion für die Basisfälle ( Empty) beendet wird. Da wir bedingungslos 0 zurückgeben, wird es beendet.
Beweisen Sie, dass die Funktion in den Nicht-Basisfällen endet ( Node). Es gibt drei Funktionsaufrufe hier: +, max, und height. Wir wissen das +und maxbeenden es, weil sie Teil der Standardbibliothek der Sprache sind und so definiert sind. Wie bereits erwähnt, dürfen wir davon ausgehen, dass die Eigenschaft, die wir zu beweisen versuchen, bei rekursiven Aufrufen wahr ist, solange sie auf unmittelbaren Teilbäumen ausgeführt werden. Daher müssen Aufrufe auch heightbeendet werden.
Damit ist der Beweis abgeschlossen. Beachten Sie, dass Sie die Beendigung mit einem Komponententest nicht nachweisen können. Jetzt heightmuss nur noch gezeigt werden, dass keine Ausnahmen ausgelöst werden.
- Beweisen Sie, dass
heightim Basisfall keine Ausnahmen auftreten ( Empty). Die Rückgabe von 0 kann keine Ausnahme auslösen, also sind wir fertig.
- Beweisen Sie, dass
heightbeim Nicht-Basisfall ( Node) keine Ausnahme ausgelöst wird . Nehmen wir noch einmal an, wir kennen +und maxwerfen keine Ausnahmen. Und die strukturelle Induktion erlaubt es uns anzunehmen, dass die rekursiven Aufrufe auch nicht werfen (weil sie die unmittelbaren Kinder des Baumes operieren). Aber warten Sie! Diese Funktion ist rekursiv, aber nicht rekursiv . Wir könnten den Stapel sprengen! Unser Beweisversuch hat einen Fehler aufgedeckt. Wir können das Problem beheben, indem wir es so ändern height, dass es rekursiv ist .
Ich hoffe, dies zeigt, dass Beweise nicht beängstigend oder kompliziert sein müssen. Tatsächlich haben Sie beim Schreiben von Code informell einen Beweis in Ihrem Kopf erstellt (andernfalls wären Sie nicht davon überzeugt, dass Sie die Funktion nur implementiert haben). Indem Sie Null, unnötige Mutation und uneingeschränkte Vererbung vermeiden, können Sie beweisen, dass Ihre Intuition ist ziemlich leicht zu korrigieren. Diese Einschränkungen sind nicht so streng, wie Sie vielleicht denken:
null ist ein Sprachfehler und es ist bedingungslos gut, ihn zu beseitigen.
- Mutationen sind manchmal unvermeidlich und notwendig, aber sie werden viel seltener benötigt, als Sie denken - insbesondere, wenn Sie über dauerhafte Datenstrukturen verfügen.
- Eine endliche Anzahl von Klassen (im funktionalen Sinne) / Unterklassen (im OOP-Sinne) gegenüber einer unbegrenzten Anzahl von Klassen zu haben, ist ein Thema, das für eine einzelne Antwort zu groß ist . Es genügt zu sagen, dass es dort einen Kompromiss zwischen Design gibt - Beweisbarkeit der Korrektheit gegenüber Flexibilität der Erweiterung.