3 คะแนน โดย GN⁺ 2024-07-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

แยกวิเคราะห์ อย่าตรวจสอบความถูกต้อง

แก่นแท้ของการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูล

  • สโลแกนสั้น ๆ สำหรับอธิบายการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูล (type-driven design): แยกวิเคราะห์ อย่าตรวจสอบความถูกต้อง
  • สโลแกนนี้หมายถึงการใช้ระบบชนิดข้อมูลเพื่อเพิ่มความปลอดภัยและความถูกต้องของโค้ด

ขอบเขตของความเป็นไปได้

  • ระบบชนิดข้อมูลแบบสแตติกช่วยให้ตัดสินได้ง่ายว่าฟังก์ชันหนึ่งสามารถ implement ได้หรือไม่
  • ตัวอย่าง: foo :: Integer -> Void ไม่สามารถ implement ได้ (Void type ไม่สามารถมีค่าได้)
  • ตัวอย่าง: ฟังก์ชัน head :: [a] -> a จะไม่มีการนิยามเมื่อรายการลิสต์ว่าง

เปลี่ยน partial function ให้เป็น total function

การจัดการความคาดหวัง
  • ฟังก์ชัน head ไม่สามารถคืนค่าได้เมื่อรายการลิสต์ว่าง จึงสามารถใช้ Maybe type เพื่อให้คืนค่า Nothing ได้
  • อย่างไรก็ตาม สิ่งนี้อาจทำให้ใช้งานได้ไม่สะดวก
ส่งต่อความคาดหวัง
  • การใช้ NonEmpty type เพื่อแทนลิสต์ที่ไม่ว่าง ช่วยรับประกันได้ว่าฟังก์ชัน head จะคืนค่าได้เสมอ
  • เมื่อใช้ NonEmpty type ก็สามารถตัดการตรวจสอบที่ไม่จำเป็นออก และให้ระบบชนิดข้อมูลจับข้อผิดพลาดได้ตั้งแต่ตอนคอมไพล์

พลังของการแยกวิเคราะห์

  • ความแตกต่างระหว่างการแยกวิเคราะห์กับการตรวจสอบความถูกต้อง อยู่ที่วิธีการเก็บรักษาข้อมูล
  • ฟังก์ชัน validateNonEmpty ตรวจสอบว่าลิสต์ไม่ว่าง แต่ไม่ได้เก็บรักษาข้อมูลนั้นไว้
  • ฟังก์ชัน parseNonEmpty ตรวจสอบว่าลิสต์ไม่ว่าง และเก็บรักษาข้อมูลนั้นไว้ด้วย NonEmpty type

ความเสี่ยงของการตรวจสอบความถูกต้อง

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

การแยกวิเคราะห์ในทางปฏิบัติ

  • ให้โฟกัสที่ชนิดข้อมูล และทำให้ type signature ของฟังก์ชันมีความเฉพาะเจาะจงมากที่สุดเท่าที่ทำได้
  • ใช้โครงสร้างข้อมูลที่ไม่สามารถแทนสถานะที่ผิดกฎหมายได้ และแปลงข้อมูลให้เป็นรูปแบบที่เฉพาะเจาะจงโดยเร็วที่สุด
  • ให้ชนิดข้อมูลเป็นตัวนำทางโค้ด ไม่ใช่ให้โค้ดเป็นตัวควบคุมชนิดข้อมูล
  • ควรใช้งานฟังก์ชันที่คืนค่า m () อย่างระมัดระวัง
  • ไม่ควรกลัวการแยกวิเคราะห์ข้อมูลหลายครั้ง
  • หลีกเลี่ยงการแทนข้อมูลในรูปแบบที่ de-normalized และหากจำเป็นควรจัดการผ่านการห่อหุ้ม (encapsulation)
  • ควรใช้ abstract data type ที่ทำให้ validator ดูเหมือน parser

สรุป ข้อคิดทบทวน และบทความอ่านเพิ่มเติม

  • การใช้ระบบชนิดข้อมูลของ Haskell ให้เกิดประโยชน์สูงสุดไม่ใช่เรื่องยาก และไม่จำเป็นต้องใช้ language extension รุ่นใหม่
  • แนวคิดหลักคือ "การเขียน total function" ซึ่งเรียบง่าย แต่ในทางปฏิบัติอาจทำได้ยาก
  • บทความอ่านเพิ่มเติมที่แนะนำ ได้แก่บล็อกโพสต์ของ Matt Parson เรื่อง "Type Safety Back and Forth" และงานเขียนของ Matt Noonan เรื่อง "Ghosts of Departed Proofs"

สรุปโดย GN⁺

  • บทความนี้อธิบายวิธีเพิ่มความปลอดภัยและความถูกต้องของโค้ดด้วยการใช้ระบบชนิดข้อมูลของ Haskell
  • เน้นย้ำว่าการเข้าใจความต่างระหว่างการแยกวิเคราะห์กับการตรวจสอบความถูกต้อง และการใช้การแยกวิเคราะห์เพื่อตรวจสอบอินพุต เป็นสิ่งสำคัญ
  • การใช้ระบบชนิดข้อมูลเพื่อสร้างโครงสร้างข้อมูลที่ไม่สามารถแทนสถานะที่ผิดกฎหมายได้ และแปลงข้อมูลให้เป็นรูปแบบที่เฉพาะเจาะจงโดยเร็วที่สุด เป็นสิ่งสำคัญ
  • บทความอ่านเพิ่มเติมที่แนะนำ ได้แก่บล็อกโพสต์ของ Matt Parson และงานเขียนของ Matt Noonan

1 ความคิดเห็น

 
GN⁺ 2024-07-23
ความคิดเห็นบน Hacker News
  • คำแนะนำและบทความนี้มีประโยชน์มาก

  • มีประโยชน์แม้กับคนที่ไม่ได้ใช้ภาษาเชิงฟังก์ชันที่มี static type

  • แนวคิดนี้ข้ามพ้นกรอบของพาราไดม์

  • แนวคิดคล้ายกันนี้พบได้ในงานเขียนสาย object-oriented ช่วงยุค 80~90 เช่น Design by Contract

  • TypeScript มักถูกเขียนในลักษณะที่ค่อย ๆ ทำให้ type เฉพาะเจาะจงขึ้นในช่วงรันไทม์

  • Design by Contract น่าจะมีอิทธิพลต่อ spec ของ Clojure (Clojure เป็นภาษาแบบ dynamic)

  • โดยพื้นฐานแล้ว นี่คือเรื่องของสมมติฐานและการรับประกัน (require กับ provide)

  • เมื่อสมมติฐานได้รับการยืนยันและมีการรับประกันแล้ว ก็ไม่จำเป็นต้องตรวจสอบสมมติฐานเดิมซ้ำในส่วนอื่นของโปรแกรม

  • เวลาเห็นในโค้ดว่ามีการตรวจสอบคุณสมบัติที่รับประกันไว้แล้วซ้ำอีก อาจทำให้สับสน และทำให้โค้ดเข้าใจและปรับปรุงได้ยากขึ้น

  • แพตเทิร์นนี้ใช้ได้ดีใน C# ยุคใหม่เช่นกัน และยังช่วยประหยัดพื้นที่ด้วย

    • โค้ดตัวอย่าง:
      if(!Whatever.TryParse<Thingy>(input, out var output)) output = some-sane-default;
      
    • โค้ดตัวอย่าง:
      if(!Whatever.TryParse<Thingy>(input, out var output)) throw new ApplicationException($"Not a valid Thingy: {input}");
      
    • แนะนำว่าอย่าใช้แบบหลังในไดรเวอร์โหมดเคอร์เนล
  • การใช้ระบบ type ที่แข็งแรงเพื่อทำให้ไม่สามารถแสดงกรณีผิดพลาดได้เลยเป็นสิ่งที่ดี และช่วยลดบั๊กซอฟต์แวร์ได้

  • มันอาจใช้เวลามากขึ้นในการคิดปัญหาและออกแบบตามนั้น แต่ในหลายกรณีก็คุ้มค่ากับเวลาที่เสียไป

  • สโลแกน "Parse, don’t validate" สรุปแนวคิดการออกแบบโดยยึด type เป็นฐานได้ดี

  • โดยส่วนตัวคิดว่าแนวทางที่ดีคือ "ทำ validation เฉพาะใน constructor เดียวเท่านั้น" เพราะจะทำให้ไม่มี object ที่ไม่ถูกต้องอยู่ได้เลย

  • หากต้องการแก้ไข object ก็ควรทำโดยเรียก constructor เดิมอีกครั้งเพื่อสร้างสถานะใหม่

  • ทำให้นึกถึง section 5 ของ qmail ซึ่งมีทั้งแนวคิด "อย่าทำ parsing" และ "มีทั้ง good interface และ user interface"

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

  • เนื้อหาที่เกี่ยวข้อง: Richard Feldman, "Making Impossible States Impossible"

  • การพูดคุยก่อนหน้า:

  • ส่งให้ Crowdstrike แล้ว

  • ทำให้นึกถึงความเห็นของใครบางคนในช่วงกระแส XML บูมกลางยุค 2000 ว่าหลายองค์กรเลือก XML เพราะ XML มาพร้อม parser

  • ทั้งที่การเขียน parser ไม่ได้ยากและยังสนุกด้วย จึงไม่ค่อยเข้าใจว่าทำไมผู้คนถึงไม่อยากเขียน parser กัน

  • สงสัยว่านี่ขัดกับความเห็นที่ว่า keyword "required" ของ Protocol Buffers เป็นความผิดพลาดครั้งใหญ่หรือไม่

  • ทางที่ดีที่สุดน่าจะเป็นการมีทั้งความสามารถในการ parse แบบยืดหยุ่นโดยไม่ตรวจสอบ และ parse แบบที่ผ่านการตรวจสอบแล้ว