사전 / 사후 조건과 불변량을 사용하여 올바른 루프를 개발하는 방법에 대한 자세한 예를 들어 보겠습니다. 이러한 주장을 함께 사양 또는 계약이라고합니다.
나는 당신이 모든 루프에 대해 이것을 시도한다고 제안하지는 않습니다. 그러나 당신이 관련된 사고 과정을 보는 것이 도움이되기를 바랍니다.
이를 위해 귀하의 분석법을 그러한 사양의 정확성을 입증하도록 설계된 Microsoft Dafny라는 도구 로 변환합니다 . 또한 각 루프의 종료를 확인합니다. 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)
.