Saya akan memberikan contoh yang lebih rinci tentang bagaimana menggunakan kondisi pra / post dan invarian untuk mengembangkan loop yang benar. Bersama-sama, pernyataan semacam itu disebut spesifikasi atau kontrak.
Saya tidak menyarankan Anda mencoba melakukan ini untuk setiap loop. Tetapi saya harap Anda akan merasakan manfaatnya jika melihat proses berpikir terlibat.
Untuk melakukannya, saya akan menerjemahkan metode Anda menjadi alat yang disebut Microsoft Dafny , yang dirancang untuk membuktikan kebenaran spesifikasi tersebut. Itu juga memeriksa penghentian setiap loop. Harap dicatat bahwa Dafny tidak memiliki for
loop, jadi saya harus menggunakan while
loop.
Akhirnya saya akan menunjukkan bagaimana Anda dapat menggunakan spesifikasi tersebut untuk mendesain versi loop Anda yang sedikit lebih sederhana. Versi loop sederhana ini ternyata memiliki kondisi loop j > 0
dan tugas array[j] = value
- seperti intuisi awal Anda.
Dafny akan membuktikan bagi kita bahwa kedua loop ini benar dan melakukan hal yang sama.
Saya kemudian akan membuat klaim umum, berdasarkan pengalaman saya, tentang bagaimana menulis loop mundur yang benar, yang mungkin akan membantu Anda jika menghadapi situasi ini di masa depan.
Bagian Satu - Menulis spesifikasi untuk metode ini
Tantangan pertama yang kita hadapi adalah menentukan apa metode yang seharusnya dilakukan. Untuk tujuan ini saya merancang kondisi sebelum dan sesudah yang menentukan perilaku metode ini. Untuk membuat spesifikasi lebih tepat, saya telah meningkatkan metode untuk mengembalikan indeks tempat value
dimasukkan.
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..])
Spesifikasi ini sepenuhnya menangkap perilaku metode. Pengamatan utama saya tentang spesifikasi ini adalah bahwa hal itu akan disederhanakan jika prosedurnya melewati nilai rightIndex+1
daripada rightIndex
. Tetapi karena saya tidak bisa melihat dari mana metode ini dipanggil, saya tidak tahu apa dampak perubahan itu terhadap sisa program.
Bagian Dua - menentukan invarian lingkaran
Sekarang kita memiliki spesifikasi untuk perilaku metode, kita harus menambahkan spesifikasi perilaku loop yang akan meyakinkan Dafny bahwa mengeksekusi loop akan berakhir dan akan menghasilkan keadaan akhir yang diinginkan array
.
Berikut ini adalah loop asli Anda, diterjemahkan ke dalam sintaks Dafny dengan invarian loop ditambahkan. Saya juga telah mengubahnya untuk mengembalikan indeks tempat nilai dimasukkan.
{
// 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
}
Ini memverifikasi di Dafny. Anda dapat melihatnya sendiri dengan mengikuti tautan ini . Jadi loop Anda tidak benar menerapkan spesifikasi metode yang saya tulis di bagian satu. Anda harus memutuskan apakah spesifikasi metode ini benar-benar perilaku yang Anda inginkan.
Perhatikan bahwa Dafny menghasilkan bukti kebenaran di sini. Ini adalah jaminan kebenaran yang jauh lebih kuat daripada yang bisa diperoleh dengan pengujian.
Bagian Tiga - loop yang lebih sederhana
Sekarang kita memiliki spesifikasi metode yang menangkap perilaku loop. Kita dapat memodifikasi implementasi loop dengan aman sambil tetap mempertahankan kepercayaan bahwa kita belum mengubah perilaku loop.
Saya telah memodifikasi loop sehingga cocok dengan intuisi asli Anda tentang kondisi loop dan nilai akhir j
. Saya berpendapat bahwa loop ini lebih sederhana daripada loop yang Anda jelaskan dalam pertanyaan Anda. Ini lebih sering dapat digunakan j
daripada j+1
.
Mulai j at rightIndex+1
Ubah kondisi loop menjadi j > 0 && arr[j-1] > value
Ubah tugas menjadi arr[j] := value
Kurangi penghitung loop di akhir loop daripada di awal
Ini kodenya. Perhatikan bahwa invarian loop juga agak lebih mudah untuk ditulis sekarang:
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;
}
Bagian Empat - saran tentang perulangan mundur
Setelah menulis dan membuktikan banyak loop yang benar selama beberapa tahun, saya memiliki saran umum berikut tentang perulangan ke belakang.
Hampir selalu lebih mudah untuk memikirkan dan menulis loop mundur (decrementing) jika penurunan dilakukan pada awal loop daripada akhir.
Sayangnya for
perulangan konstruksi dalam banyak bahasa membuat ini sulit.
Saya curiga (tetapi tidak dapat membuktikan) bahwa kompleksitas ini adalah apa yang menyebabkan perbedaan dalam intuisi Anda tentang apa yang seharusnya menjadi loop dan apa yang sebenarnya dibutuhkan. Anda terbiasa berpikir tentang loop ke depan (incrementing). Ketika Anda ingin menulis loop mundur (decrementing) Anda mencoba untuk membuat loop dengan mencoba membalikkan urutan hal-hal yang terjadi dalam loop maju (bertambah). Tetapi karena cara for
konstruksinya bekerja Anda lalai untuk membalik urutan penugasan dan pembaruan variabel putaran - yang diperlukan untuk pembalikan yang benar dari urutan operasi antara loop mundur dan maju.
Bagian Lima - bonus
Hanya untuk kelengkapan, berikut adalah kode yang Anda dapatkan jika Anda meneruskan rightIndex+1
ke metode daripada rightIndex
. Perubahan ini menghilangkan semua +2
offset yang diminta untuk memikirkan kebenaran loop.
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
kesalahan? Saya akan lebih waspada dengan fakta bahwa Anda mengaksesarray[j]
danarray[j + 1]
tanpa terlebih dahulu memeriksa ituarray.length > (j + 1)
.