Tôi sẽ đưa ra một ví dụ chi tiết hơn về cách sử dụng các điều kiện trước / sau và bất biến để phát triển một vòng lặp chính xác. Cùng với các xác nhận như vậy được gọi là một đặc điểm kỹ thuật hoặc hợp đồng.
Tôi không gợi ý rằng bạn cố gắng làm điều này cho mỗi vòng lặp. Nhưng tôi hy vọng rằng bạn sẽ thấy hữu ích khi thấy quá trình suy nghĩ liên quan.
Để làm như vậy, tôi sẽ dịch phương thức của bạn thành một công cụ có tên Microsoft Dafny , được thiết kế để chứng minh tính chính xác của các thông số kỹ thuật đó. Nó cũng kiểm tra chấm dứt của mỗi vòng lặp. Xin lưu ý rằng Dafny không có for
vòng lặp nên tôi phải sử dụng while
vòng lặp thay thế.
Cuối cùng tôi sẽ chỉ ra làm thế nào bạn có thể sử dụng các thông số kỹ thuật như vậy để thiết kế một phiên bản vòng lặp đơn giản hơn một chút. Phiên bản vòng lặp đơn giản hơn này không có điều kiện vòng lặp j > 0
và bài tập array[j] = value
- như trực giác ban đầu của bạn.
Dafny sẽ chứng minh cho chúng tôi rằng cả hai vòng lặp này đều đúng và làm cùng một việc.
Sau đó tôi sẽ đưa ra một tuyên bố chung, dựa trên kinh nghiệm của tôi, về cách viết vòng lặp ngược chính xác, điều đó có thể sẽ giúp bạn nếu gặp phải tình huống này trong tương lai.
Phần một - Viết một đặc tả cho phương thức
Thách thức đầu tiên chúng ta phải đối mặt là xác định phương pháp thực sự phải làm là gì. Cuối cùng, tôi thiết kế các điều kiện trước và sau trong đó xác định hành vi của phương thức. Để làm cho đặc tả chính xác hơn, tôi đã tăng cường phương thức để làm cho nó trả về chỉ mục nơi value
được chèn.
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..])
Đặc điểm kỹ thuật này nắm bắt đầy đủ hành vi của phương pháp. Quan sát chính của tôi về đặc tả này là nó sẽ được đơn giản hóa nếu thủ tục được thông qua giá trị rightIndex+1
chứ không phải rightIndex
. Nhưng vì tôi không thể thấy phương thức này được gọi từ đâu nên tôi không biết sự thay đổi đó sẽ có ảnh hưởng gì đến phần còn lại của chương trình.
Phần thứ hai - xác định một bất biến vòng lặp
Bây giờ chúng ta có một đặc tả cho hành vi của phương thức, chúng ta phải thêm một đặc tả về hành vi vòng lặp sẽ thuyết phục Dafny rằng việc thực thi vòng lặp sẽ chấm dứt và sẽ dẫn đến trạng thái cuối cùng mong muốn array
.
Sau đây là vòng lặp ban đầu của bạn, được dịch sang cú pháp Dafny với các bất biến vòng lặp được thêm vào. Tôi cũng đã thay đổi nó để trả về chỉ mục nơi giá trị được chèn vào.
{
// 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
}
Điều này xác minh trong Dafny. Bạn có thể nhìn thấy nó bằng cách theo liên kết này . Vì vậy, vòng lặp của bạn thực hiện chính xác đặc tả phương thức mà tôi đã viết trong phần một. Bạn sẽ cần phải quyết định xem đặc tả phương thức này có thực sự là hành vi mà bạn muốn hay không.
Lưu ý rằng Dafny đang tạo ra một bằng chứng chính xác ở đây. Đây là một đảm bảo chính xác mạnh mẽ hơn nhiều so với có thể có được bằng cách thử nghiệm.
Phần thứ ba - một vòng lặp đơn giản hơn
Bây giờ chúng ta có một đặc tả phương thức nắm bắt hành vi của vòng lặp. Chúng ta có thể sửa đổi một cách an toàn việc thực hiện vòng lặp trong khi vẫn giữ niềm tin rằng chúng ta không thay đổi hành vi của vòng lặp.
Tôi đã sửa đổi vòng lặp để nó phù hợp với trực giác ban đầu của bạn về điều kiện vòng lặp và giá trị cuối cùng của j
. Tôi sẽ lập luận rằng vòng lặp này đơn giản hơn vòng lặp mà bạn mô tả trong câu hỏi của bạn. Nó thường có thể sử dụng j
hơn là j+1
.
Bắt đầu j tại rightIndex+1
Thay đổi điều kiện vòng lặp thành j > 0 && arr[j-1] > value
Thay đổi nhiệm vụ thành arr[j] := value
Giảm bộ đếm vòng lặp ở cuối vòng lặp thay vì bắt đầu
Đây là mã. Lưu ý rằng các bất biến vòng lặp cũng có phần dễ viết hơn bây giờ:
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;
}
Phần thứ tư - lời khuyên về vòng lặp ngược
Sau khi đã viết và chứng minh đúng nhiều vòng lặp trong một vài năm, tôi có lời khuyên chung sau đây về việc lặp lại.
Hầu như luôn luôn dễ dàng hơn để suy nghĩ và viết một vòng lặp (giảm dần) nếu việc giảm được thực hiện ở đầu vòng lặp hơn là kết thúc.
Thật không may for
cấu trúc vòng lặp trong nhiều ngôn ngữ làm cho điều này trở nên khó khăn.
Tôi nghi ngờ (nhưng không thể chứng minh) rằng sự phức tạp này là nguyên nhân gây ra sự khác biệt trong trực giác của bạn về vòng lặp nên là gì và nó thực sự cần phải là gì. Bạn đã quen nghĩ về các vòng lặp (tăng dần). Khi bạn muốn viết một vòng lặp (giảm dần), bạn cố gắng tạo vòng lặp bằng cách cố gắng đảo ngược thứ tự mà mọi thứ xảy ra trong một vòng lặp (tăng dần). Nhưng do cách thức thi for
công mà bạn đã bỏ qua để đảo ngược thứ tự cập nhật biến số gán và vòng lặp - điều cần thiết cho một sự đảo ngược thực sự của thứ tự các hoạt động giữa một vòng lặp ngược và tiến.
Phần thứ năm - tiền thưởng
Để cho đầy đủ, đây là mã bạn nhận được nếu bạn chuyển rightIndex+1
sang phương thức chứ không phải rightIndex
. Những thay đổi này giúp loại bỏ tất cả các +2
độ lệch được yêu cầu để suy nghĩ về tính đúng đắn của vòng lặp.
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
là một sai lầm? Tôi sẽ cảnh giác hơn với thực tế là bạn đang truy cậparray[j]
vàarray[j + 1]
không kiểm tra trướcarray.length > (j + 1)
.