ฉันจะยกตัวอย่างรายละเอียดเพิ่มเติมเกี่ยวกับวิธีใช้เงื่อนไขก่อน / หลังและค่าคงที่เพื่อพัฒนาวงที่ถูกต้อง ร่วมกันยืนยันดังกล่าวเรียกว่าข้อกำหนดหรือสัญญา
ฉันไม่แนะนำให้คุณลองทำเช่นนี้กับทุกลูป แต่ฉันหวังว่าคุณจะพบว่ามีประโยชน์ในการดูกระบวนการคิดที่เกี่ยวข้อง
ในการดำเนินการดังกล่าวฉันจะแปลวิธีการของคุณเป็นเครื่องมือที่เรียกว่า Microsoft Dafnyซึ่งออกแบบมาเพื่อพิสูจน์ความถูกต้องของข้อกำหนดดังกล่าว นอกจากนี้ยังตรวจสอบการเลิกจ้างของแต่ละวง โปรดทราบว่า Dafny ไม่มีการfor
วนซ้ำดังนั้นฉันจึงต้องใช้การwhile
วนซ้ำแทน
ในที่สุดฉันจะแสดงวิธีที่คุณสามารถใช้ข้อมูลจำเพาะดังกล่าวเพื่อออกแบบลูปเวอร์ชันของคุณที่ง่ายขึ้นเล็กน้อย เวอร์ชั่นลูปที่ง่ายกว่านี้คือ infact มีเงื่อนไขของลูป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)