我将给出一个更详细的示例,说明如何使用前置/后置条件和不变式来开发正确的循环。此类断言一起称为规范或合同。
我不建议您尝试为每个循环执行此操作。但是我希望您会发现查看所涉及的思维过程很有用。
为此,我将把您的方法转换为一个名为Microsoft Dafny的工具,该工具旨在证明此类规范的正确性。它还检查每个循环的终止。请注意,Dafny没有for
循环,因此我不得不使用while
循环。
最后,我将展示如何使用这些规范来设计一个可以说是稍微简单些的循环版本。这个更简单的循环版本实际上确实具有循环条件j > 0
和赋值array[j] = value
-就像您的最初直觉一样。
Dafny将为我们证明这两个循环都是正确的,并且做同样的事情。
然后,根据我的经验,我将对如何编写正确的向后循环做出一般性声明,如果将来遇到这种情况,这可能会对您有所帮助。
第一部分-编写方法规范
我们面临的第一个挑战是确定该方法的实际作用。为此,我设计了指定方法行为的前提条件和条件条件。为了使规范更加精确,我增强了方法以使其返回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
。但是由于我看不到从何处调用此方法,所以我不知道更改将对程序的其余部分产生什么影响。
第二部分-确定循环不变性
现在我们有了方法行为的规范,我们必须添加循环行为的规范,这将使Dafny确信执行循环将终止并导致所需的最终状态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中进行了验证。您可以通过以下链接自己查看。因此,您的循环可以正确实现我在第一部分中编写的方法规范。您将需要确定此方法规范是否确实是您想要的行为。
请注意,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)
。