Dam bardziej szczegółowy przykład użycia warunków przed / po i niezmienników do opracowania poprawnej pętli. Razem takie twierdzenia nazywane są specyfikacją lub umową.
Nie sugeruję, abyś próbował to zrobić dla każdej pętli. Ale mam nadzieję, że przyda ci się proces myślowy.
W tym celu przełożę twoją metodę na narzędzie o nazwie Microsoft Dafny , które ma na celu udowodnienie poprawności takich specyfikacji. Sprawdza także zakończenie każdej pętli. Pamiętaj, że Dafny nie ma for
pętli, więc while
zamiast tego musiałem użyć pętli.
Na koniec pokażę, jak wykorzystać takie specyfikacje do zaprojektowania, prawdopodobnie, nieco prostszej wersji pętli. Ta prostsza wersja pętli faktycznie ma warunek pętli j > 0
i przypisanie array[j] = value
- tak jak początkowa intuicja.
Dafny udowodni nam, że obie pętle są prawidłowe i robią to samo.
W oparciu o moje doświadczenie przedstawię ogólne twierdzenie o tym, jak napisać prawidłową pętlę wsteczną, która być może pomoże ci w obliczu takiej sytuacji w przyszłości.
Część pierwsza - Pisanie specyfikacji dla metody
Pierwszym wyzwaniem, przed którym stoimy, jest określenie, co właściwie ma zrobić ta metoda. W tym celu zaprojektowałem warunki wstępne i końcowe, które określają zachowanie metody. Aby uczynić specyfikację bardziej dokładną, ulepszyłem metodę, aby zwracała indeks, w którym value
została wstawiona.
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..])
Ta specyfikacja w pełni oddaje zachowanie metody. Moje główne spostrzeżenie na temat tej specyfikacji jest takie, że uproszczenie byłoby, gdyby procedura przeszła wartość rightIndex+1
zamiast rightIndex
. Ale ponieważ nie widzę, skąd ta metoda jest wywoływana, nie wiem, jaki wpływ ta zmiana miałaby na resztę programu.
Część druga - określenie niezmiennika pętli
Teraz mamy specyfikację zachowania metody, musimy dodać specyfikację zachowania pętli, która przekona Dafny'ego, że wykonanie pętli zostanie zakończone i spowoduje pożądany stan końcowy array
.
Oto twoja oryginalna pętla, przetłumaczona na składnię Dafny z dodanymi niezmiennikami pętli. Zmieniłem go również, aby zwrócić indeks, do którego wstawiono wartość.
{
// 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
}
Sprawdza to w Dafny. Możesz to zobaczyć samodzielnie, klikając ten link . Więc twoja pętla poprawnie implementuje specyfikację metody, którą napisałem w części pierwszej. Musisz zdecydować, czy ta specyfikacja metody jest rzeczywiście zachowaniem, które chciałeś.
Pamiętaj, że Dafny przedstawia tutaj dowód poprawności. Jest to znacznie silniejsza gwarancja poprawności, niż można ją uzyskać testując.
Część trzecia - prostsza pętla
Teraz, gdy mamy specyfikację metody, która przechwytuje zachowanie pętli. Możemy bezpiecznie modyfikować implementację pętli zachowując pewność, że nie zmieniliśmy zachowania pętli.
Zmodyfikowałem pętlę, aby pasowała do twoich pierwotnych intuicji dotyczących stanu pętli i końcowej wartości j
. Twierdziłbym, że ta pętla jest prostsza niż pętla opisana w pytaniu. Częściej jest w stanie używać j
niż j+1
.
Rozpocznij j o rightIndex+1
Zmień warunek pętli na j > 0 && arr[j-1] > value
Zmień przydział na arr[j] := value
Zmniejsz licznik pętli na końcu pętli, a nie na początku
Oto kod. Zauważ, że niezmienniki pętli są teraz nieco łatwiejsze do napisania:
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;
}
Część czwarta - porady dotyczące zapętlania wstecznego
Po napisaniu i sprawdzeniu poprawności wielu pętli przez dość kilka lat, mam następującą ogólną radę na temat zapętlania wstecz.
Prawie zawsze łatwiej jest pomyśleć i napisać pętlę wsteczną (dekrementującą), jeśli dekrementacja jest wykonywana na początku pętli, a nie na jej końcu.
Niestety for
konstrukcja pętli w wielu językach sprawia, że jest to trudne.
Podejrzewam (ale nie mogę udowodnić), że ta złożoność spowodowała różnicę w twojej intuicji na temat tego, czym powinna być pętla i jaka powinna być. Jesteś przyzwyczajony do myślenia o pętlach do przodu (zwiększających). Gdy chcesz napisać pętlę wsteczną (zmniejszającą), próbuj utworzyć pętlę, próbując odwrócić kolejność, w jakiej dzieje się w pętli przewijającej (zwiększającej). Ale ze względu na sposób działania for
konstrukcji zaniedbałeś odwrócenie kolejności przypisania i aktualizacji zmiennej pętli - co jest potrzebne do prawdziwego odwrócenia kolejności operacji między pętlą wsteczną i następną.
Część piąta - bonus
Dla kompletności, oto kod, który otrzymujesz, jeśli przejdziesz rightIndex+1
do metody, a nie rightIndex
. Zmiany te eliminują wszystkie +2
przesunięcia, które w innym przypadku byłyby wymagane, aby pomyśleć o poprawności pętli.
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
to pomyłka? Byłbym bardziej ostrożny wobec tego, że uzyskujesz dostęparray[j]
iarray[j + 1]
bez uprzedniej kontroliarray.length > (j + 1)
.