แยกวิเคราะห์ อย่าตรวจสอบความถูกต้อง
แก่นแท้ของการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูล
- สโลแกนสั้น ๆ สำหรับอธิบายการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูล (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 ความคิดเห็น
ความคิดเห็นบน Hacker News
คำแนะนำและบทความนี้มีประโยชน์มาก
มีประโยชน์แม้กับคนที่ไม่ได้ใช้ภาษาเชิงฟังก์ชันที่มี static type
แนวคิดนี้ข้ามพ้นกรอบของพาราไดม์
แนวคิดคล้ายกันนี้พบได้ในงานเขียนสาย object-oriented ช่วงยุค 80~90 เช่น Design by Contract
TypeScript มักถูกเขียนในลักษณะที่ค่อย ๆ ทำให้ type เฉพาะเจาะจงขึ้นในช่วงรันไทม์
Design by Contract น่าจะมีอิทธิพลต่อ spec ของ Clojure (Clojure เป็นภาษาแบบ dynamic)
โดยพื้นฐานแล้ว นี่คือเรื่องของสมมติฐานและการรับประกัน (require กับ provide)
เมื่อสมมติฐานได้รับการยืนยันและมีการรับประกันแล้ว ก็ไม่จำเป็นต้องตรวจสอบสมมติฐานเดิมซ้ำในส่วนอื่นของโปรแกรม
เวลาเห็นในโค้ดว่ามีการตรวจสอบคุณสมบัติที่รับประกันไว้แล้วซ้ำอีก อาจทำให้สับสน และทำให้โค้ดเข้าใจและปรับปรุงได้ยากขึ้น
แพตเทิร์นนี้ใช้ได้ดีใน C# ยุคใหม่เช่นกัน และยังช่วยประหยัดพื้นที่ด้วย
การใช้ระบบ 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 แบบที่ผ่านการตรวจสอบแล้ว