事前/事後条件と不変条件を使用して正しいループを作成する方法のより詳細な例を示します。このようなアサーションをまとめて、仕様または契約と呼びます。
ループごとにこれを行うことをお勧めするわけではありません。しかし、関係する思考プロセスを見ることが役立つと思います。
そのために、メソッドをMicrosoft Dafnyというツールに変換します。MicrosoftDafnyは、このような仕様の正確性を証明するように設計されています。また、各ループの終了もチェックします。Dafnyにはfor
ループがないため、while
代わりにループを使用する必要があることに注意してください。
最後に、このような仕様を使用して、ほぼ間違いなく簡単なバージョンのループを設計する方法を示します。この単純なループバージョンには、実際にはループ条件j > 0
と割り当てarray[j] = value
があります-最初の直感と同じです。
Dafnyは、これらのループの両方が正しいことを証明し、同じことを行います。
次に、経験に基づいて、正しい逆方向ループを記述する方法について一般的な主張を行います。これは、将来このような状況に直面した場合に役立つでしょう。
パート1-メソッドの仕様を記述する
私たちが直面する最初の課題は、メソッドが実際に行うべきことを決定することです。このため、メソッドの動作を指定する事前条件と事後条件を設計しました。仕様をより正確にするために、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
。しかし、このメソッドがどこから呼び出されたのかわからないため、その変更がプログラムの残りの部分にどのような影響を与えるかはわかりません。
パート2-ループ不変量の決定
メソッドの動作の仕様ができたので、ループの実行の仕様を追加して、ループの実行が終了し、の最終状態になることを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で検証します。このリンクをたどると、自分で確認できます。そのため、ループは、パート1で書いたメソッド仕様を正しく実装しています。このメソッド仕様が本当に望んだ動作であるかどうかを判断する必要があります。
ここでDafnyが正確さの証拠を作成していることに注意してください。これは、テストで得られる可能性よりもはるかに強力な正確性の保証です。
パート3-より単純なループ
これで、ループの動作をキャプチャするメソッド仕様ができました。ループの動作を変更していないという自信を保ちながら、ループの実装を安全に変更できます。
ループの条件と最終値に関する元の直感に一致するようにループを変更しました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;
}
パート4-後方ループに関するアドバイス
かなりの数年にわたって多くのループを作成し、正しいことを証明した後、逆方向のループに関する次の一般的なアドバイスを受け取ります。
ほとんどの場合、ループの最後ではなく最初にデクリメントが実行される場合、逆方向(デクリメント)ループを考えて記述する方が簡単です。
残念ながら、for
多くの言語のループ構造はこれを難しくしています。
この複雑さが、ループがどうあるべきか、実際に必要なものについての直感に違いを生じさせたのではないかと思う(しかし証明できない)。あなたは順方向(増分)ループについて考えることに慣れています。逆方向(デクリメント)ループを作成する場合、順方向(インクリメント)ループで発生する順序を逆にしようとしてループを作成しようとします。しかし、for
コンストラクトの動作方法のため、割り当てとループ変数の更新の順序を逆にすることを怠っています。これは、逆方向ループと順方向ループの間で操作の順序を真に逆にするために必要です。
パート5-ボーナス
完全を期すために、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)
。