Ein Beweis ist in der OOP-Welt aufgrund von Nebenwirkungen, uneingeschränkter Vererbung und null
der 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, tree
dessen Werte in genau zwei Varianten (oder Klassen, nicht zu verwechseln mit dem OOP-Konzept einer Klasse) vorliegen können - ein Empty
Wert, der keine Informationen enthält, und Node
Werte, die ein 3-Tupel enthalten, dessen erster und letzter Elemente sind tree
s 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 max
Funktion 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 height
Funktion nach Fällen definiert - es gibt eine Definition für Empty
Bäume und eine Definition für Node
Bä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
, value
und rightChild
jeweils 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 height
richtig implementiert haben? Wir können eine strukturelle Induktion verwenden , die aus Folgendem besteht: 1. Beweisen Sie, dass die Basisfälle height
unseres tree
Typs ( Empty
) height
korrekt sind ( ). 2. Unter der Annahme, dass rekursive Aufrufe korrekt sind, beweisen Sie, dass sie height
fü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 Empty
Baum 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 height
Rü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 max
beenden 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 height
beendet werden.
Damit ist der Beweis abgeschlossen. Beachten Sie, dass Sie die Beendigung mit einem Komponententest nicht nachweisen können. Jetzt height
muss nur noch gezeigt werden, dass keine Ausnahmen ausgelöst werden.
- Beweisen Sie, dass
height
im Basisfall keine Ausnahmen auftreten ( Empty
). Die Rückgabe von 0 kann keine Ausnahme auslösen, also sind wir fertig.
- Beweisen Sie, dass
height
beim Nicht-Basisfall ( Node
) keine Ausnahme ausgelöst wird . Nehmen wir noch einmal an, wir kennen +
und max
werfen 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.