ในการเขียนโปรแกรมการทำงานหนึ่งจะบรรลุ modularity ผ่านกฎหมายทางคณิตศาสตร์ได้อย่างไร


11

ฉันอ่านคำถามนี้ว่าโปรแกรมเมอร์ที่ใช้งานมักจะใช้การพิสูจน์ทางคณิตศาสตร์เพื่อให้แน่ใจว่าโปรแกรมทำงานได้อย่างถูกต้อง ฟังดูง่ายกว่าและเร็วกว่าการทดสอบหน่วย แต่มาจากพื้นหลังการทดสอบ OOP / หน่วยที่ฉันไม่เคยเห็นมาก่อน

คุณช่วยอธิบายให้ฉันฟังและยกตัวอย่างได้ไหม


7
"ฟังดูง่ายกว่าและเร็วกว่าการทดสอบหน่วย" ใช่เสียง ในความเป็นจริงมันเป็นไปไม่ได้ในทางปฏิบัติสำหรับซอฟต์แวร์ส่วนใหญ่ และทำไมถึงกล่าวถึงชื่อแบบแยกส่วน แต่คุณกำลังพูดถึงการตรวจสอบ?
สุข

@Euphoric ในการทดสอบหน่วยใน OOP คุณเขียนการทดสอบเพื่อตรวจสอบ ... การตรวจสอบว่าส่วนหนึ่งของซอฟต์แวร์ทำงานอย่างถูกต้อง แต่ยังตรวจสอบว่าข้อกังวลของคุณจะถูกแยกออก ... เช่น modularity และ reusablity ... ถ้าฉันเข้าใจอย่างถูกต้อง
leeand00

2
@Euphoric เฉพาะในกรณีที่คุณละเมิดการกลายพันธุ์และการสืบทอดและทำงานในภาษาที่มีระบบพิมพ์ที่มีข้อบกพร่อง (เช่นมีnull)
Doval

@ leeand00 ฉันคิดว่าคุณใช้คำว่า "การยืนยัน" ในทางที่ผิด ความถูกต้องของโมดูลและการใช้ซ้ำไม่ได้ถูกตรวจสอบโดยตรงโดยการตรวจสอบซอฟต์แวร์ (แน่นอนว่าการขาดโมดูลนั้นสามารถทำให้ซอฟต์แวร์ยากต่อการดูแลรักษาและนำมาใช้ซ้ำดังนั้นจึงแนะนำข้อบกพร่องและกระบวนการตรวจสอบล้มเหลว)
Andres F.

มันง่ายกว่ามากในการตรวจสอบชิ้นส่วนของซอฟต์แวร์ถ้ามันเขียนในลักษณะโมดูลาร์ ดังนั้นคุณสามารถมีหลักฐานจริงที่ฟังก์ชั่นการทำงานอย่างถูกต้องสำหรับบางฟังก์ชั่นสำหรับคนอื่น ๆ ที่คุณสามารถเขียนการทดสอบหน่วย
grizwako

คำตอบ:


22

การพิสูจน์นั้นยากกว่าในโลกของ OOP เนื่องจากผลข้างเคียงการสืบทอดที่ไม่ จำกัด และnullการเป็นสมาชิกของทุกประเภท หลักฐานส่วนใหญ่ใช้หลักการอุปนัยเพื่อแสดงให้เห็นว่าคุณได้ครอบคลุมทุกความเป็นไปได้และสิ่งทั้งสามนั้นทำให้ยากต่อการพิสูจน์

สมมติว่าเรากำลังใช้ต้นไม้ไบนารีที่มีค่าจำนวนเต็ม (เพื่อรักษาไวยากรณ์ให้ง่ายขึ้นฉันจะไม่นำการเขียนโปรแกรมทั่วไปมาใช้ในสิ่งนี้แม้ว่ามันจะไม่เปลี่ยนแปลงอะไรเลย) ใน ML มาตรฐานฉันจะกำหนดเช่นนั้น นี้:

datatype tree = Empty | Node of (tree * int * tree)

สิ่งนี้นำเสนอชนิดใหม่ที่เรียกว่าtreeค่าที่สามารถมีได้สองพันธุ์ (หรือคลาสเพื่อไม่ให้สับสนกับแนวคิด OOP ของคลาส) - Emptyค่าที่ไม่มีข้อมูลและNodeค่าที่มี 3-tuple ซึ่งเป็นอันดับแรกและสุดท้าย องค์ประกอบและองค์ประกอบกลางซึ่งเป็นtree intการประมาณการประกาศนี้ใน OOP ที่ใกล้เคียงที่สุดจะเป็นดังนี้:

public class Tree {
    private Tree() {} // Prevent external subclassing

    public static final class Empty extends Tree {}

    public static final class Node extends Tree {
        public final Tree leftChild;
        public final int value;
        public final Tree rightChild;

        public Node(Tree leftChild, int value, Tree rightChild) {
            this.leftChild = leftChild;
            this.value = value;
            this.rightChild = rightChild;
        }
    }
}

ด้วยข้อแม้ที่ตัวแปรประเภทต้นไม้ไม่สามารถเป็นnullได้

ทีนี้ลองเขียนฟังก์ชั่นเพื่อคำนวณความสูง (หรือความลึก) ของทรีและสมมติว่าเราเข้าถึงmaxฟังก์ชั่นที่คืนค่าตัวเลขสองตัวที่ใหญ่กว่า:

fun height(Empty) =
        0
 |  height(Node (leftChild, value, rightChild)) =
        1 + max( height(leftChild), height(rightChild) )

เราได้นิยามheightฟังก์ชันตามกรณี - มีหนึ่งคำนิยามสำหรับEmptyต้นไม้และอีกหนึ่งคำนิยามสำหรับNodeต้นไม้ คอมไพเลอร์รู้จำนวนต้นไม้ที่มีอยู่และจะส่งคำเตือนหากคุณไม่ได้กำหนดทั้งสองกรณี การแสดงออกNode (leftChild, value, rightChild)ในลายเซ็นของฟังก์ชั่นผูกค่าของ 3 tuple เพื่อตัวแปรleftChild, valueและrightChildตามลำดับเพื่อให้เราสามารถหมายถึงพวกเขาในความหมายฟังก์ชั่น มันคล้ายกับการประกาศตัวแปรท้องถิ่นเช่นนี้ในภาษา OOP:

Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();

เราจะพิสูจน์ได้heightอย่างไรว่าเราได้ปฏิบัติอย่างถูกต้อง? เราสามารถใช้การเหนี่ยวนำโครงสร้างซึ่งประกอบด้วย: 1. พิสูจน์ว่าheightถูกต้องในกรณีพื้นฐาน (s) treeประเภทของเรา( Empty) 2. สมมติว่าการเรียกซ้ำheightจะถูกต้องพิสูจน์ว่าheightถูกต้องสำหรับกรณีที่ไม่ใช่ฐาน ) (เมื่อต้นไม้เป็นจริงNode)

สำหรับขั้นตอนที่ 1 เราจะเห็นว่าฟังก์ชันส่งกลับค่า 0 เสมอเมื่ออาร์กิวเมนต์เป็นEmptyแผนผัง สิ่งนี้ถูกต้องตามคำจำกัดความของความสูงของต้นไม้

สำหรับขั้นตอนที่ 2 1 + max( height(leftChild), height(rightChild) )ผลตอบแทนที่ฟังก์ชั่น สมมติว่าการเรียกซ้ำแบบเรียกคืนความสูงของเด็กอย่างแท้จริงเราจะเห็นว่าสิ่งนี้ถูกต้องเช่นกัน

และนั่นก็เป็นการพิสูจน์ที่สมบูรณ์ ขั้นตอนที่ 1 และ 2 รวมกันหมดความเป็นไปได้ทั้งหมด อย่างไรก็ตามโปรดทราบว่าเราไม่มีการกลายพันธุ์ไม่มีโมฆะและมีต้นไม้สองสายพันธุ์ นำเงื่อนไขทั้งสามนี้ไปใช้และการพิสูจน์อย่างรวดเร็วจะซับซ้อนมากขึ้นหากไม่สามารถปฏิบัติได้


แก้ไข:เนื่องจากคำตอบนี้ได้เพิ่มขึ้นไปด้านบนฉันต้องการเพิ่มตัวอย่างเล็กน้อยของการพิสูจน์และครอบคลุมการเหนี่ยวนำโครงสร้างครอบคลุมอีกเล็กน้อย ข้างต้นเราพิสูจน์แล้วว่าถ้าheightผลตอบแทนค่าส่งคืนนั้นถูกต้อง เราไม่ได้พิสูจน์ว่ามันจะส่งคืนค่าเสมอ เราสามารถใช้การเหนี่ยวนำเชิงโครงสร้างเพื่อพิสูจน์สิ่งนี้ได้เช่นกัน (หรือคุณสมบัติอื่น ๆ ) อีกครั้งในระหว่างขั้นตอนที่ 2 เราได้รับอนุญาตให้สมมติว่าการพักสายของการเรียกซ้ำเกิดขึ้นตราบใดที่การเรียกซ้ำเกิดซ้ำทั้งหมด ต้นไม้.

ฟังก์ชั่นสามารถล้มเหลวในการคืนค่าในสองสถานการณ์: ถ้ามันโยนข้อยกเว้นและถ้ามันวนซ้ำตลอดไป ก่อนอื่นเรามาพิสูจน์ว่าหากไม่มีข้อยกเว้นเกิดขึ้นฟังก์ชันจะยุติลง:

  1. พิสูจน์ว่า (หากไม่มีข้อยกเว้นถูกโยนทิ้ง) ฟังก์ชั่นจะยุติลงสำหรับเคสพื้นฐาน ( Empty) เนื่องจากเราคืนค่า 0 โดยไม่มีเงื่อนไขจึงสิ้นสุดลง

  2. พิสูจน์ว่าฟังก์ชั่นนี้สิ้นสุดในกรณีที่ไม่ใช่ฐาน ( Node) มีสามสายฟังก์ชั่นที่นี่: +, และmax heightเรารู้+และmaxยุติเพราะพวกเขาเป็นส่วนหนึ่งของห้องสมุดมาตรฐานของภาษาและพวกเขาได้กำหนดไว้อย่างนั้น ดังที่ได้กล่าวไว้ก่อนหน้านี้เราได้รับอนุญาตให้สมมติคุณสมบัติที่เรากำลังพยายามพิสูจน์ว่าเป็นจริงในการโทรแบบเรียกซ้ำตราบใดที่พวกเขาทำงานบนทรีย่อยทันทีดังนั้นการโทรก็จะheightยุติลงเช่นกัน

สรุปได้ว่าหลักฐาน โปรดทราบว่าคุณจะไม่สามารถพิสูจน์การสิ้นสุดด้วยการทดสอบหน่วย ตอนนี้สิ่งที่เหลืออยู่ก็คือการแสดงที่heightไม่ส่งข้อยกเว้น

  1. พิสูจน์ว่าheightไม่มีข้อยกเว้นในกรณีพื้นฐาน ( Empty) การส่งคืน 0 ไม่สามารถโยนข้อยกเว้นได้เราจึงเสร็จสิ้น
  2. พิสูจน์ว่าheightไม่มีข้อยกเว้นในกรณีที่ไม่ใช่ฐาน ( Node) สมมติอีกครั้งว่าเรารู้+และmaxไม่โยนข้อยกเว้น และการเหนี่ยวนำเชิงโครงสร้างช่วยให้เราสามารถรับสายเรียกซ้ำไม่ได้เช่นกัน (เพราะทำงานกับลูก ๆ ของต้นไม้) แต่เดี๋ยวก่อน! ฟังก์ชั่นนี้เป็น recursive แต่ไม่recursive หาง เราสามารถระเบิดกอง! ข้อพิสูจน์ที่เราพยายามทำพบข้อผิดพลาด เราสามารถแก้ไขได้ด้วยการเปลี่ยนheightไปเป็นหาง recursive

ฉันหวังว่าสิ่งนี้แสดงให้เห็นว่าการพิสูจน์ไม่จำเป็นต้องน่ากลัวหรือซับซ้อน ในความเป็นจริงเมื่อใดก็ตามที่คุณเขียนโค้ดคุณได้สร้างหลักฐานไว้ในหัวอย่างไม่เป็นทางการ (ไม่เช่นนั้นคุณจะไม่เชื่อเลยว่าคุณเพิ่งใช้ฟังก์ชัน) โดยการหลีกเลี่ยงค่า Null การกลายพันธุ์ที่ไม่จำเป็นและการสืบทอดแบบไม่ จำกัด ถูกต้องค่อนข้างง่าย ข้อ จำกัด เหล่านี้ไม่รุนแรงเท่าที่คุณคิด:

  • null เป็นข้อบกพร่องทางภาษาและการทำไปด้วยดีไม่มีเงื่อนไข
  • บางครั้งการกลายพันธุ์นั้นเป็นสิ่งที่หลีกเลี่ยงไม่ได้และจำเป็น แต่ก็จำเป็นน้อยกว่าที่คุณคิดโดยเฉพาะเมื่อคุณมีโครงสร้างข้อมูลถาวร
  • ขณะที่มีจำนวน จำกัด ของการเรียน (ในความรู้สึกที่ทำงาน) / subclasses (ในความหมาย OOP) ที่เทียบได้ไม่ จำกัด จำนวนของพวกเขาว่าเป็นเรื่องใหญ่เกินไปสำหรับคำตอบเดียว พอจะพูดได้ว่ามีการแลกเปลี่ยนการออกแบบ - ความสามารถในการพิสูจน์ความถูกต้องและความยืดหยุ่นในการขยาย

8
  1. มันเป็นง่ายมากที่จะเหตุผลเกี่ยวกับรหัสเมื่อทุกอย่างจะไม่เปลี่ยนรูป เป็นผลให้ลูปมักจะถูกเขียนซ้ำเป็นการเรียกซ้ำ โดยทั่วไปการตรวจสอบความถูกต้องของโซลูชันแบบเรียกซ้ำได้ง่ายขึ้น บ่อยครั้งวิธีแก้ปัญหาดังกล่าวจะอ่านคล้ายกับนิยามทางคณิตศาสตร์ของปัญหา

    อย่างไรก็ตามมีแรงจูงใจน้อยมากในการพิสูจน์ความถูกต้องอย่างเป็นทางการในกรณีส่วนใหญ่ พิสูจน์ได้ยากใช้เวลามาก (มนุษย์) และมี ROI ต่ำ

  2. ภาษาที่ใช้งานได้บางส่วน (โดยเฉพาะจากตระกูล ML) มีระบบการพิมพ์ที่แสดงออกอย่างมากซึ่งสามารถรับประกันได้ว่าระบบประเภท C (แต่ความคิดบางอย่างเช่นภาษาอังกฤษทั่วไปได้กลายเป็นเรื่องธรรมดาไปแล้วในภาษากระแสหลัก) เมื่อโปรแกรมผ่านการตรวจสอบประเภทนี่เป็นหลักฐานอัตโนมัติ ในบางกรณีสิ่งนี้จะสามารถตรวจพบข้อผิดพลาดบางอย่าง (เช่นลืมกรณีฐานในการเรียกซ้ำหรือลืมจัดการบางกรณีในรูปแบบการจับคู่)

    ในทางกลับกันระบบประเภทเหล่านี้จะต้องถูก จำกัด อย่างมากเพื่อให้สามารถตัดสินใจได้ ดังนั้นในความรู้สึกเราได้รับการค้ำประกันแบบคงที่โดยการให้ความยืดหยุ่นขึ้น - และข้อ จำกัด เหล่านี้เป็นเหตุผลที่ซับซ้อนเอกสารทางวิชาการตามสายของ“เป็นวิธีการแก้ปัญหาเอกไปแก้ไขปัญหาใน Haskell ” ที่มีอยู่

    ฉันสนุกกับทั้งภาษาเสรีนิยมและภาษาที่ จำกัด อย่างมากและทั้งสองมีปัญหาตามลำดับ แต่ไม่ใช่ในกรณีที่ว่า "ดีกว่า" แต่ละอย่างจะสะดวกกว่าสำหรับงานประเภทอื่น

จากนั้นจะต้องมีการชี้ให้เห็นว่าการพิสูจน์และการทดสอบหน่วยไม่สามารถใช้แทนกันได้ พวกเขาทั้งสองอนุญาตให้เรากำหนดขอบเขตความถูกต้องของโปรแกรม:

  • การทดสอบให้ขอบเขตที่ถูกต้อง: หากการทดสอบล้มเหลวโปรแกรมไม่ถูกต้องหากไม่มีการทดสอบล้มเหลวเรามั่นใจว่าโปรแกรมจะจัดการกับกรณีทดสอบ แต่อาจยังมีข้อผิดพลาดที่ยังไม่ได้เปิด

    int factorial(int n) {
      if (n <= 1) return 1;
      if (n == 2) return 2;
      if (n == 3) return 6;
      return -1;
    }
    
    assert(factorial(0) == 1);
    assert(factorial(1) == 1);
    assert(factorial(3) == 6);
    // oops, we forgot to test that it handles n > 3…
    
  • หลักฐานแสดงขอบเขตที่ต่ำกว่าในความถูกต้อง: มันเป็นไปไม่ได้ที่จะพิสูจน์คุณสมบัติบางอย่าง ตัวอย่างเช่นมันอาจเป็นเรื่องง่ายที่จะพิสูจน์ว่าฟังก์ชั่นคืนค่าตัวเลขเสมอ (นั่นคือสิ่งที่ระบบประเภททำ) < 10แต่มันอาจจะเป็นไปไม่ได้ที่จะพิสูจน์ว่าตัวเลขจะเป็น

    int factorial(int n) {
      return n;  // FIXME this is just a placeholder to make it compile
    }
    
    // type system says this will be OK…
    

1
"อาจเป็นไปไม่ได้ที่จะพิสูจน์คุณสมบัติบางอย่าง ... แต่มันอาจเป็นไปไม่ได้ที่จะพิสูจน์ว่าจำนวนนั้นจะเป็น <10" หากความถูกต้องของโปรแกรมขึ้นอยู่กับจำนวนที่น้อยกว่า 10 คุณควรจะสามารถพิสูจน์ได้ มันเป็นความจริงที่ระบบประเภทไม่สามารถทำได้ (อย่างน้อยก็ไม่ใช่หากไม่มีโปรแกรมที่ถูกต้องมากมาย) - แต่คุณทำได้
Doval

@Doval ใช่ อย่างไรก็ตามระบบพิมพ์เป็นเพียงตัวอย่างของระบบสำหรับการพิสูจน์ ระบบประเภทนั้น จำกัด อย่างเห็นได้ชัดและไม่สามารถประเมินความจริงของข้อความบางอย่างได้ บุคคลสามารถดำเนินการพิสูจน์ที่ซับซ้อนมากขึ้นอย่างมากมาย แต่ยังคงมีข้อ จำกัด ในสิ่งที่เขาสามารถพิสูจน์ได้ ยังมีข้อ จำกัด ที่ไม่สามารถข้ามได้มันอยู่ไกลออกไป
amon

1
ตกลงฉันแค่คิดว่าตัวอย่างที่ทำให้เข้าใจผิดเล็กน้อย
Doval

2
ในภาษาที่พิมพ์อย่างพึ่งพาเช่นไอดริสอาจเป็นไปได้ที่จะพิสูจน์ว่าผลตอบแทนต่ำกว่า 10
Ingo

2
บางทีวิธีที่ดีกว่าในการจัดการกับข้อกังวลที่ @Doval เกิดขึ้นอาจกล่าวได้ว่าปัญหาบางอย่างนั้นไม่สามารถตัดสินใจได้ (เช่นปัญหาการหยุดชะงัก) ต้องใช้เวลามากเกินไปในการพิสูจน์หรือต้องมีการค้นพบคณิตศาสตร์ใหม่เพื่อพิสูจน์ผลลัพธ์ ความเห็นส่วนตัวของฉันคือคุณควรชี้แจงว่าหากมีสิ่งใดพิสูจน์ได้ว่าเป็นจริงคุณไม่จำเป็นต้องทดสอบหน่วย พิสูจน์แล้วทำให้ขอบเขตบนและล่าง สาเหตุที่การพิสูจน์และการทดสอบไม่สามารถใช้แทนกันได้เพราะการพิสูจน์อาจยากเกินกว่าที่จะทำหรือเป็นไปไม่ได้ที่จะทำ นอกจากนี้การทดสอบสามารถเป็นไปโดยอัตโนมัติ (สำหรับเมื่อมีการเปลี่ยนแปลงรหัส)
Thomas Eding

7

คำเตือนอาจอยู่ในลำดับที่นี่:

ในขณะที่มันเป็นความจริงโดยทั่วไปสิ่งที่คนอื่นเขียนที่นี่ - ในระยะสั้นระบบขั้นสูงประเภทที่ไม่เปลี่ยนแปลงและความโปร่งใสในการอ้างอิงทำให้เกิดความถูกต้องมาก - ไม่ใช่กรณีที่การทดสอบไม่ได้เกิดขึ้นในโลกการทำงาน ในทางตรงกันข้าม !

นี่เป็นเพราะเรามีเครื่องมือเช่น Quickcheck ที่สร้างกรณีทดสอบโดยอัตโนมัติและสุ่ม คุณเพียงแค่ระบุกฎหมายว่าฟังก์ชั่นจะต้องเชื่อฟังและจากนั้น QuickCheck จะตรวจสอบกฎหมายเหล่านี้ว่ามีกรณีทดสอบหลายร้อยรายการ

คุณจะเห็นว่านี่เป็นระดับที่สูงกว่าการตรวจสอบความเท่าเทียมกันเล็กน้อยในกรณีทดสอบจำนวนหนึ่ง

นี่คือตัวอย่างจากการใช้งานทรี AVL:

--- A generator for arbitrary Trees with integer keys and string values
aTree = arbitrary :: Gen (Tree Int String)


--- After insertion, a lookup with the same key yields the inserted value        
p_insert = forAll aTree (\t -> 
             forAll arbitrary (\k ->
               forAll arbitrary (\v ->
                lookup (insert t k v) k == Just v)))

--- After deletion of a key, lookup results in Nothing
p_delete = forAll aTree (\t ->
            not (null t) ==> forAll (elements (keys t)) (\k ->
                lookup (delete t k) k == Nothing))

กฎข้อที่สอง (หรือคุณสมบัติ) ที่เราสามารถอ่านได้ดังต่อไปนี้: สำหรับต้นไม้ที่ไม่มีกฎเกณฑ์ทั้งหมดจะมีสิ่งtต่อไปนี้: ถ้า tไม่ว่างเปล่าสำหรับคีย์ทั้งหมดkของต้นไม้นั้นมันจะเก็บสิ่งนั้นไว้kในต้นไม้kจากtผลลัพธ์จะเป็นNothing(ซึ่งระบุว่า: ไม่พบ)

สิ่งนี้จะตรวจสอบการทำงานที่เหมาะสมสำหรับการลบคีย์ที่มีอยู่ กฎหมายใดที่ควรควบคุมการลบคีย์ที่ไม่มีอยู่ แน่นอนว่าเราต้องการให้ต้นไม้ที่ได้นั้นเหมือนกับต้นไม้ที่เราลบออกไป เราสามารถแสดงสิ่งนี้ได้อย่างง่ายดาย:

p_delete_nonexistant = forAll aTree (\t ->
                          forAll arbitrary (\k -> 
                              k `notElem` keys t ==> delete t k == t))

วิธีนี้การทดสอบสนุกจริงๆ และนอกจากนี้เมื่อคุณเรียนรู้ที่จะอ่าน QuickCheck คุณสมบัติที่พวกเขาทำหน้าที่เป็นสเปคเครื่องทดสอบ


4

ฉันไม่เข้าใจความหมายของคำตอบที่เชื่อมโยงกันโดย "บรรลุโมดุลริตี้ผ่านกฎทางคณิตศาสตร์" แต่ฉันคิดว่าฉันมีความคิดว่ามีความหมายอะไร

ตรวจสอบFunctor :

คลาส Functor มีการกำหนดดังนี้:

 class Functor f where
   fmap :: (a -> b) -> f a -> f b

มันไม่ได้มาพร้อมกับกรณีทดสอบ แต่มีกฎหมายสองข้อที่ต้องทำให้พอใจ

ทุกกรณีของ Functor ควรเป็นไปตาม:

 fmap id = id
 fmap (p . q) = (fmap p) . (fmap q)

สมมติว่าคุณใช้งานFunctor( แหล่งที่มา ):

instance  Functor Maybe  where
    fmap _ Nothing       = Nothing
    fmap f (Just a)      = Just (f a)

ปัญหาคือการตรวจสอบว่าการใช้งานของคุณเป็นไปตามกฎหมาย คุณจะทำเช่นนั้นได้อย่างไร

วิธีหนึ่งคือการเขียนกรณีทดสอบ ข้อ จำกัด พื้นฐานของวิธีนี้คือคุณกำลังตรวจสอบพฤติกรรมในจำนวนกรณีที่ จำกัด (ขอให้โชคดีในการทดสอบฟังก์ชั่นด้วยพารามิเตอร์ 8 ตัว!

อีกวิธีหนึ่งคือการใช้การใช้เหตุผลเชิงคณิตศาสตร์เช่นการพิสูจน์ตามความหมายที่แท้จริง (แทนการใช้พฤติกรรมในกรณีจำนวน จำกัด ) ความคิดที่นี่คือการพิสูจน์ทางคณิตศาสตร์อาจมีประสิทธิภาพมากขึ้น อย่างไรก็ตามสิ่งนี้ขึ้นอยู่กับว่าโปรแกรมของคุณรองรับการพิสูจน์ทางคณิตศาสตร์ได้อย่างไร

ฉันไม่สามารถแนะนำคุณผ่านการพิสูจน์อย่างเป็นทางการจริง ๆ ว่าFunctorตัวอย่างข้างต้นเป็นไปตามกฎหมาย แต่ฉันจะพยายามและให้รายละเอียดของสิ่งที่หลักฐานอาจมีลักษณะ:

  1. fmap id = id
    • ถ้าเรามี Nothing
      • fmap id Nothing= Nothingโดยส่วนที่ 1 ของการนำไปใช้
      • id Nothing= Nothingตามคำจำกัดความของid
    • ถ้าเรามี Just x
      • fmap id (Just x)= Just (id x)= Just xโดยส่วนที่ 2 ของการนำไปใช้งานแล้วตามด้วยคำจำกัดความของid
  2. fmap (p . q) = (fmap p) . (fmap q)
    • ถ้าเรามี Nothing
      • fmap (p . q) Nothing= Nothingโดยส่วนที่ 1
      • (fmap p) . (fmap q) $ Nothing= (fmap p) $ Nothing= Nothingโดยสองแอปพลิเคชันของส่วนที่ 1
    • ถ้าเรามี Just x
      • fmap (p . q) (Just x)= Just ((p . q) x)= Just (p (q x))โดยส่วนที่ 2 จากนั้นตามคำจำกัดความของ.
      • (fmap p) . (fmap q) $ (Just x)= (fmap p) $ (Just (q x))= Just (p (q x))โดยสองแอปพลิเคชั่นของส่วนที่สอง

-1

"ระวังข้อบกพร่องในโค้ดด้านบน; ฉันได้พิสูจน์แล้วว่าถูกต้องเท่านั้น, ไม่ได้ทดลองใช้" - Donald Knuth

ในโลกที่สมบูรณ์แบบโปรแกรมเมอร์นั้นสมบูรณ์แบบและไม่ทำผิดพลาดดังนั้นจึงไม่มีข้อบกพร่อง

ในโลกที่สมบูรณ์แบบนักวิทยาศาสตร์คอมพิวเตอร์และนักคณิตศาสตร์ก็สมบูรณ์แบบและไม่ทำผิดพลาดเช่นกัน

แต่เราไม่ได้อยู่ในโลกที่สมบูรณ์แบบ ดังนั้นเราจึงไม่สามารถพึ่งพาโปรแกรมเมอร์ไม่ให้ทำผิดพลาด แต่เราไม่สามารถสรุปได้ว่านักวิทยาศาสตร์คอมพิวเตอร์คนใดที่ให้การพิสูจน์ทางคณิตศาสตร์ว่าโปรแกรมถูกต้องไม่ได้ทำผิดพลาดในการพิสูจน์ ดังนั้นฉันจะไม่ให้ความสนใจกับใครก็ตามที่พยายามพิสูจน์ว่ารหัสของเขาใช้ได้ เขียนการทดสอบหน่วยและแสดงให้ฉันเห็นว่ารหัสทำงานตามข้อกำหนด สิ่งอื่นใดที่ไม่ทำให้ฉันเชื่อมั่น


5
การทดสอบหน่วยสามารถมีข้อผิดพลาดได้เช่นกัน ที่สำคัญการทดสอบสามารถแสดงตัวตนของแมลงได้ - ไม่เคยขาดหายไป @Ingo กล่าวในคำตอบของเขาพวกเขาทำการตรวจสุขภาพที่ดีและพิสูจน์อย่างสมบูรณ์ แต่พวกเขาไม่ได้ทดแทนสำหรับพวกเขา
Doval
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.