Я надам більш детальний приклад того, як використовувати умови до / після та інваріанти для розробки правильного циклу. Разом такі твердження називаються специфікацією або контрактом.
Я не пропоную вам намагатися робити це для кожного циклу. Але я сподіваюся, що вам буде корисно побачити процес думки.
Для цього я переведу ваш метод на інструмент під назвою Microsoft Dafny , який призначений для підтвердження правильності таких специфікацій. Він також перевіряє завершення кожного циклу. Зауважте, що у Дафні немає for
циклу, тому мені довелося while
замість цього використовувати цикл.
Нарешті я покажу, як ви можете використовувати такі технічні характеристики для створення, мабуть, трохи простішої версії вашого циклу. Ця простіша версія циклу дійсно впливає на стан циклу j > 0
та призначення array[j] = value
- як це було у вашої початкової інтуїції.
Дафні докаже для нас, що обидві ці петлі є правильними і роблять те саме.
Тоді я буду висловлювати загальну претензію на основі свого досвіду щодо того, як написати правильний зворотний цикл, який, можливо, допоможе вам, якщо ви зіткнулися з цією ситуацією в майбутньому.
Частина перша - написання специфікації методу
Перше завдання, з яким ми стикаємося, - це визначити, чим насправді належить зробити метод. З цією метою я розробив умови до і після, які визначають поведінку методу. Щоб уточнити специфікацію, я вдосконалив метод, щоб змусити його повернути індекс, куди value
було вставлено.
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..])
Ця специфікація повністю фіксує поведінку методу. Моє основне зауваження щодо цієї специфікації - це було б спрощено, якби процедура передала значення, rightIndex+1
а не значення rightIndex
. Але оскільки я не бачу, звідки цей метод викликаний, я не знаю, який вплив ця зміна матиме на решту програми.
Частина друга - визначення інваріантного циклу
Тепер у нас є специфікація поведінки методу, ми повинні додати специфікацію поведінки циклу, яка переконає Дафні в тому, що виконання циклу припиняється і призведе до бажаного остаточного стану array
.
Далі - ваш оригінальний цикл, перекладений у синтаксис Dafny із доданими циклічними інваріантами. Я також змінив його, щоб повернути індекс, куди було вставлено значення.
{
// 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
}
Це підтверджується в Dafny. Ви можете переконатися у цьому самі, перейшовши за цим посиланням . Отже, ваш цикл правильно реалізує специфікацію методу, яку я написав у першій частині. Вам потрібно буде визначитися, чи справді специфікація цього методу є поведінкою, якої ви хотіли.
Зауважте, що Дафні надає тут доказ коректності. Це набагато сильніша гарантія правильності, ніж це можливо отримати шляхом тестування.
Частина третя - простіший цикл
Тепер, коли у нас є специфікація методу, яка фіксує поведінку циклу. Ми можемо безпечно змінювати реалізацію циклу, зберігаючи впевненість, що ми не змінили поведінку циклу.
Я змінив цикл, щоб він відповідав вашим оригінальним інтуїціям про стан циклу та кінцеве значення j
. Я б заперечував, що ця петля простіша за цикл, який ви описали у своєму запитанні. Її частіше можна використовувати, j
а не використовувати j+1
.
Почніть j в rightIndex+1
Змініть стан циклу на j > 0 && arr[j-1] > value
Змініть призначення на arr[j] := value
Зменшення лічильника циклу на кінці циклу, а не на початку
Ось код. Зауважимо, що зараз інваріанти циклу також дещо простіше написати:
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;
}
Частина четверта - поради щодо зворотнього циклу
Після написання та підтвердження правильності багатьох циклів протягом декількох років, я маю наступні загальні поради щодо циклу назад.
Практично завжди простіше подумати і записати зворотний (декрементний) цикл, якщо декремент виконується на початку циклу, а не в кінці.
На жаль, for
конфігурація циклу на багатьох мовах ускладнює це.
Я підозрюю (але не можу довести), що ця складність - це те, що спричинило різницю у вашій інтуїції щодо того, яким повинен бути цикл і яким він насправді повинен бути. Ви звикли думати про передні (збільшувальні) петлі. Коли ви хочете записати зворотний (декрементний) цикл, ви намагаєтеся створити цикл, намагаючись змінити порядок того, що відбувається в прямому (збільшувальному) циклі. Але через те, як for
працює ця конструкція, ви знехтували, щоб змінити порядок оновлення змінної циклу та оновлення - що потрібно для справжнього перетворення порядку операцій між зворотним і прямим циклом.
Частина п'ята - премія
Просто для повноти, ось код, який ви отримаєте, якщо перейти rightIndex+1
до методу, а не rightIndex
. Ці зміни виключають усі +2
компенсації, які в іншому випадку вимагають думати про правильність циклу.
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
це помилка? Мені б з більшою обережністю ставився до того, що ви отримуєте доступarray[j]
таarray[j + 1]
не попередньо перевіряючи цеarray.length > (j + 1)
.