Я собираюсь привести более подробный пример того, как использовать предварительные / последующие условия и инварианты для разработки правильного цикла. Вместе такие утверждения называются спецификацией или контрактом.
Я не предлагаю вам пытаться делать это для каждого цикла. Но я надеюсь, что вам будет полезно увидеть процесс мышления.
Для этого я переведу ваш метод в инструмент под названием Microsoft Dafny , который призван доказать правильность таких спецификаций. Он также проверяет завершение каждого цикла. Обратите внимание, что у 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
}
Это подтверждается в Дафни. Вы можете увидеть это сами, перейдя по этой ссылке . Таким образом, ваш цикл правильно реализует спецификацию метода, которую я написал в первой части. Вам нужно будет решить, действительно ли эта спецификация метода соответствует желаемому.
Обратите внимание, что Дафни дает здесь подтверждение правильности. Это гораздо более надежная гарантия правильности, чем может быть получена путем тестирования.
Часть третья - более простая петля
Теперь, когда у нас есть спецификация метода, которая фиксирует поведение цикла. Мы можем безопасно изменить реализацию цикла, сохраняя при этом уверенность в том, что мы не изменили поведение цикла.
Я изменил цикл так, чтобы он соответствовал вашей первоначальной интуиции относительно условия цикла и конечного значения 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)
.