Ho intenzione di dare un esempio più dettagliato di come utilizzare le condizioni pre / post e gli invarianti per sviluppare un ciclo corretto. Insieme, tali affermazioni sono chiamate specifiche o contratto.
Non sto suggerendo di provare a farlo per ogni ciclo. Spero che troverai utile vedere coinvolto il processo di pensiero.
Per fare ciò, tradurrò il tuo metodo in uno strumento chiamato Microsoft Dafny , progettato per dimostrare la correttezza di tali specifiche. Controlla anche la chiusura di ciascun loop. Si noti che Dafny non ha un for
ciclo, quindi ho dovuto usare un while
ciclo invece.
Infine, mostrerò come è possibile utilizzare tali specifiche per progettare una versione, probabilmente, leggermente più semplice del tuo loop. Questa versione più semplice del loop ha infatti le condizioni del loop j > 0
e l'assegnazione array[j] = value
, così come l'intuizione iniziale.
Dafny dimostrerà per noi che entrambi questi loop sono corretti e fanno la stessa cosa.
Farò quindi un reclamo generale, basato sulla mia esperienza, su come scrivere un corretto ciclo al contrario, che forse ti aiuterà se dovessi affrontare questa situazione in futuro.
Prima parte - Scrivere una specifica per il metodo
La prima sfida che dobbiamo affrontare è determinare cosa dovrebbe effettivamente fare il metodo. A tal fine ho progettato le condizioni pre e post che specificano il comportamento del metodo. Per rendere più precisa la specifica, ho migliorato il metodo per farlo restituire l'indice in cui è value
stato inserito.
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..])
Questa specifica cattura completamente il comportamento del metodo. La mia osservazione principale su questa specifica è che sarebbe semplificato se alla procedura fosse passato il valore rightIndex+1
anziché rightIndex
. Ma dal momento che non riesco a vedere da dove viene chiamato questo metodo, non so quale effetto avrebbe avuto quel cambiamento sul resto del programma.
Parte seconda: determinare un invariante di loop
Ora abbiamo una specifica per il comportamento del metodo, dobbiamo aggiungere una specifica del comportamento del ciclo che convincerà Dafny che l'esecuzione del ciclo terminerà e porterà allo stato finale desiderato array
.
Quello che segue è il tuo loop originale, tradotto nella sintassi di Dafny con gli invarianti di loop aggiunti. L'ho anche modificato per restituire l'indice in cui è stato inserito il valore.
{
// 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
}
Ciò si verifica in Dafny. Puoi vederlo da solo seguendo questo link . Quindi il tuo loop implementa correttamente le specifiche del metodo che ho scritto nella prima parte. Dovrai decidere se questa specifica del metodo è davvero il comportamento che volevi.
Nota che Dafny sta producendo una prova di correttezza qui. Questa è una garanzia di correttezza molto più forte di quella che si può ottenere dai test.
Terza parte: un ciclo più semplice
Ora che abbiamo una specifica del metodo che cattura il comportamento del loop. Siamo in grado di modificare in modo sicuro l'implementazione del loop pur mantenendo la sicurezza di non aver modificato il comportamento del loop.
Ho modificato il loop in modo che corrisponda alle tue intuizioni originali sulla condizione del loop e sul valore finale di j
. Direi che questo loop è più semplice del loop che hai descritto nella tua domanda. È più spesso in grado di utilizzare j
piuttosto che j+1
.
Inizia j alle rightIndex+1
Cambia la condizione del loop in j > 0 && arr[j-1] > value
Cambia il compito in arr[j] := value
Decrementa il contatore del loop alla fine del loop anziché all'inizio
Ecco il codice Nota che gli invarianti di loop sono anche un po 'più facili da scrivere ora:
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;
}
Quarta parte: consigli sul looping all'indietro
Dopo aver scritto e dimostrato molti loop corretti per diversi anni, ho i seguenti consigli generali sul looping all'indietro.
È quasi sempre più facile pensare e scrivere un loop all'indietro (in decremento) se il decremento viene eseguito all'inizio del loop piuttosto che alla fine.
Sfortunatamente il for
costrutto del ciclo in molte lingue lo rende difficile.
Sospetto (ma non posso provare) che questa complessità sia ciò che ha causato la differenza nella tua intuizione su ciò che dovrebbe essere il ciclo e ciò che effettivamente doveva essere. Sei abituato a pensare ai loop forward (incrementali). Quando si desidera scrivere un ciclo all'indietro (in decremento), si tenta di creare il ciclo tentando di invertire l'ordine in cui le cose accadono in un ciclo in avanti (in aumento). Ma a causa del modo in cui funziona il for
costrutto, hai trascurato di invertire l'ordine delle assegnazioni e l'aggiornamento delle variabili del ciclo, che è necessario per una vera inversione dell'ordine delle operazioni tra un ciclo indietro e un ciclo avanti.
Parte quinta: bonus
Solo per completezza, ecco il codice che ottieni se passi rightIndex+1
al metodo anziché rightIndex
. Questa modifica elimina tutti gli +2
offset che sarebbero altrimenti necessari per pensare alla correttezza del loop.
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
sia un errore? Sarei più cauto del fatto che tu stia accedendoarray[j]
earray[j + 1]
senza prima verificarloarray.length > (j + 1)
.