Ich werde ein detaillierteres Beispiel geben, wie man Pre / Post-Bedingungen und Invarianten verwendet, um eine korrekte Schleife zu entwickeln. Zusammen werden solche Behauptungen als Spezifikation oder Vertrag bezeichnet.
Ich schlage nicht vor, dass Sie versuchen, dies für jede Schleife zu tun. Aber ich hoffe, dass Sie es nützlich finden, den beteiligten Denkprozess zu sehen.
Zu diesem Zweck übersetze ich Ihre Methode in ein Tool namens Microsoft Dafny , mit dem die Richtigkeit solcher Spezifikationen nachgewiesen werden soll. Es prüft auch die Beendigung jeder Schleife. Bitte beachten Sie, dass Dafny keine for
Schleife hat und ich while
stattdessen eine Schleife verwenden musste.
Abschließend werde ich zeigen, wie Sie solche Spezifikationen verwenden können, um eine wohl etwas einfachere Version Ihrer Schleife zu entwerfen. Diese einfachere Loop-Version hat tatsächlich die Loop-Bedingung j > 0
und die Zuweisung array[j] = value
- so wie es Ihre anfängliche Intuition war.
Dafny wird uns beweisen, dass beide Schleifen korrekt sind und dasselbe tun.
Ich werde dann, basierend auf meinen Erfahrungen, einen allgemeinen Anspruch darauf erheben, wie man eine korrekte Rückwärtsschleife schreibt, der Ihnen vielleicht helfen wird, wenn Sie in Zukunft mit dieser Situation konfrontiert werden.
Erster Teil - Schreiben einer Spezifikation für die Methode
Die erste Herausforderung besteht darin, festzustellen, was die Methode tatsächlich leisten soll. Zu diesem Zweck habe ich Vor- und Nachbedingungen entworfen, die das Verhalten der Methode festlegen. Um die Spezifikation genauer zu machen, habe ich die Methode dahingehend erweitert, dass sie den Index zurückgibt, an dem sie value
eingefügt wurde.
method insert(arr:array<int>, rightIndex:int, value:int) returns (index:int)
// the method will modify the array
modifies arr
// the array will not be null
requires arr != null
// the right index is within the bounds of the array
// but not the last item
requires 0 <= rightIndex < arr.Length - 1
// value will be inserted into the array at index
ensures arr[index] == value
// index is within the bounds of the array
ensures 0 <= index <= rightIndex + 1
// the array to the left of index is not modified
ensures arr[..index] == old(arr[..index])
// the array to the right of index, up to right index is
// shifted to the right by one place
ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
// the array to the right of rightIndex+1 is not modified
ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
Diese Spezifikation erfasst das Verhalten der Methode vollständig. Meine Hauptbemerkung zu dieser Spezifikation ist, dass es vereinfacht würde, wenn der Prozedur der Wert übergeben würde, rightIndex+1
anstatt rightIndex
. Da ich jedoch nicht sehen kann, woher diese Methode stammt, weiß ich nicht, welche Auswirkungen diese Änderung auf den Rest des Programms haben würde.
Zweiter Teil - Bestimmung einer Schleifeninvariante
Jetzt haben wir eine Spezifikation für das Verhalten der Methode. Wir müssen eine Spezifikation für das Schleifenverhalten hinzufügen, die Dafny davon überzeugt, dass die Ausführung der Schleife beendet wird und zum gewünschten Endzustand von führt array
.
Das Folgende ist Ihre ursprüngliche Schleife, übersetzt in Dafny-Syntax mit hinzugefügten Schleifeninvarianten. Ich habe es auch geändert, um den Index zurückzugeben, in dem Wert eingefügt wurde.
{
// take a copy of the initial array, so we can refer to it later
// ghost variables do not affect program execution, they are just
// for specification
ghost var initialArr := arr[..];
var j := rightIndex;
while(j >= 0 && arr[j] > value)
// the loop always decreases j, so it will terminate
decreases j
// j remains within the loop index off-by-one
invariant -1 <= j < arr.Length
// the right side of the array is not modified
invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
// the part of the array looked at by the loop so far is
// shifted by one place to the right
invariant arr[j+2..rightIndex+2] == initialArr[j+1..rightIndex+1]
// the part of the array not looked at yet is not modified
invariant arr[..j+1] == initialArr[..j+1]
{
arr[j + 1] := arr[j];
j := j-1;
}
arr[j + 1] := value;
return j+1; // return the position of the insert
}
Dies wird in Dafny bestätigt. Sie können es selbst sehen, indem Sie diesem Link folgen . Ihre Schleife implementiert also die Methodenspezifikation, die ich in Teil 1 geschrieben habe, korrekt. Sie müssen entscheiden, ob diese Methodenspezifikation wirklich das gewünschte Verhalten ist.
Beachten Sie, dass Dafny hier einen Beweis für die Richtigkeit vorlegt. Dies ist eine weitaus stärkere Garantie für die Richtigkeit, als dies durch Tests möglich ist.
Dritter Teil - eine einfachere Schleife
Jetzt haben wir eine Methodenspezifikation, die das Verhalten der Schleife erfasst. Wir können die Implementierung der Schleife sicher ändern, ohne jedoch die Gewissheit zu verlieren, dass wir das Verhalten der Schleife nicht geändert haben.
Ich habe die Schleife so modifiziert, dass sie Ihren ursprünglichen Vorstellungen über den Schleifenzustand und den Endwert von entspricht j
. Ich würde argumentieren, dass diese Schleife einfacher ist als die Schleife, die Sie in Ihrer Frage beschrieben haben. Es ist häufiger in der Lage, j
als zu verwenden j+1
.
Beginne j um rightIndex+1
Ändern Sie die Schleifenbedingung in j > 0 && arr[j-1] > value
Ändern Sie die Zuordnung zu arr[j] := value
Dekrementieren Sie den Schleifenzähler am Ende der Schleife und nicht am Anfang
Hier ist der Code. Beachten Sie, dass die Schleifeninvarianten jetzt auch etwas einfacher zu schreiben sind:
method insert2(arr:array<int>, rightIndex:int, value:int) returns (index:int)
modifies arr
requires arr != null
requires 0 <= rightIndex < arr.Length - 1
ensures 0 <= index <= rightIndex + 1
ensures arr[..index] == old(arr[..index])
ensures arr[index] == value
ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
{
ghost var initialArr := arr[..];
var j := rightIndex+1;
while(j > 0 && arr[j-1] > value)
decreases j
invariant 0 <= j <= arr.Length
invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
invariant arr[j+1..rightIndex+2] == initialArr[j..rightIndex+1]
invariant arr[..j] == initialArr[..j]
{
j := j-1;
arr[j + 1] := arr[j];
}
arr[j] := value;
return j;
}
Vierter Teil - Hinweise zum Rückwärtsschleifen
Nachdem ich über einige Jahre viele Schleifen geschrieben und als richtig erwiesen habe, habe ich den folgenden allgemeinen Ratschlag zum Zurückschleifen.
Es ist fast immer einfacher, über eine Rückwärtsschleife (Dekrementierungsschleife) nachzudenken und diese zu schreiben, wenn die Dekrementierung am Anfang der Schleife und nicht am Ende durchgeführt wird.
Leider macht das for
Schleifenkonstrukt in vielen Sprachen dies schwierig.
Ich vermute (kann aber nicht beweisen), dass diese Komplexität den Unterschied in Ihrer Intuition darüber verursacht hat, was die Schleife sein sollte und was sie eigentlich sein musste. Sie sind es gewohnt, über vorwärts gerichtete (inkrementelle) Schleifen nachzudenken. Wenn Sie eine Rückwärtsschleife (Dekrementierungsschleife) schreiben möchten, versuchen Sie, die Schleife zu erstellen, indem Sie versuchen, die Reihenfolge umzukehren, in der die Vorwärtsschleife (Inkrementierungsschleife) ausgeführt wird. Aufgrund der Funktionsweise des for
Konstrukts haben Sie jedoch versäumt, die Reihenfolge der Zuweisung und die Aktualisierung der Schleifenvariablen umzukehren. Dies ist für eine echte Umkehrung der Reihenfolge der Operationen zwischen einer Rückwärts- und einer Vorwärtsschleife erforderlich.
Fünfter Teil - Bonus
Der Vollständigkeit halber sehen Sie hier den Code, den Sie erhalten, wenn Sie rightIndex+1
die Methode anstelle von übergeben rightIndex
. Diese Änderung beseitigt alle +2
Offsets, die ansonsten erforderlich sind, um über die Richtigkeit der Schleife nachzudenken.
method insert3(arr:array<int>, rightIndex:int, value:int) returns (index:int)
modifies arr
requires arr != null
requires 1 <= rightIndex < arr.Length
ensures 0 <= index <= rightIndex
ensures arr[..index] == old(arr[..index])
ensures arr[index] == value
ensures arr[index+1..rightIndex+1] == old(arr[index..rightIndex])
ensures arr[rightIndex+1..] == old(arr[rightIndex+1..])
{
ghost var initialArr := arr[..];
var j := rightIndex;
while(j > 0 && arr[j-1] > value)
decreases j
invariant 0 <= j <= arr.Length
invariant arr[rightIndex+1..] == initialArr[rightIndex+1..]
invariant arr[j+1..rightIndex+1] == initialArr[j..rightIndex]
invariant arr[..j] == initialArr[..j]
{
j := j-1;
arr[j + 1] := arr[j];
}
arr[j] := value;
return j;
}
j >= 0
für einen Fehler? Ich wäre vorsichtiger, wenn Sie darauf zugreifenarray[j]
und diesarray[j + 1]
nicht vorher überprüfen würdenarray.length > (j + 1)
.