3 คะแนน โดย GN⁺ 2024-07-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูล การ พาร์ส ซึ่งเก็บผลการตรวจสอบไว้เป็นชนิดข้อมูลที่แม่นยำกว่า จะช่วยเพิ่มความปลอดภัยของโค้ดภายหลังได้มากกว่าการ ตรวจสอบความถูกต้อง ที่เพียงตรวจอินพุตแล้วทิ้งผลไป
  • ฟังก์ชันที่ล้มเหลวกับอินพุตบางส่วนได้ เช่น head:: [a] -> a อาจทำให้ชนิดข้อมูลของค่าที่ส่งคืนอ่อนลงได้ แต่ผู้เรียกจะต้องแบกรับกิ่งเงื่อนไขความล้มเหลวที่ไม่จำเป็นต่อไปเรื่อย ๆ
  • NonEmpty a เก็บสถานะที่ไม่ใช่ลิสต์ว่างไว้ในชนิดข้อมูล ช่วยลดการตรวจซ้ำและการจัดการข้อผิดพลาดที่ “ไม่มีทางเกิดขึ้น”
  • หากแทรกการตรวจสอบเฉพาะหน้าไว้ทั่วโค้ดประมวลผล จะกลายเป็น shotgun parsing และอาจพบข้อผิดพลาดของอินพุตก็ต่อเมื่อมีการเปลี่ยนแปลงสถานะบางอย่างไปแล้ว
  • ในงานจริง ควรใส่รูปแทนข้อมูลที่ต้องการไว้ในซิกเนเจอร์ของฟังก์ชันก่อน แล้วใช้ Map, ชนิดข้อมูลนามธรรม, smart constructor ฯลฯ เพื่อยก เงื่อนไขคงสภาพ ไปไว้ที่ขอบเขตของชนิดข้อมูล

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

  • “Parse, don’t validate” เป็นวลีที่ย่อแนวคิดการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูลไว้ในสามคำ
  • ระบบชนิดข้อมูลแบบสแตติกช่วยเผยคำถามว่า “ฟังก์ชันนี้เขียนได้หรือไม่” ก่อนลงมือเขียนโค้ด
  • ในตัวอย่าง Haskell foo :: Integer -> Void ไม่สามารถสร้างค่าจริงได้ เพราะ Void ไม่มีค่าอยู่เลย
  • head :: [a] -> a ก็ไม่ได้ถูกนิยามสำหรับทุกอินพุต เพราะอาจได้รับลิสต์ว่าง []
    • GHC จะเตือนว่า pattern matching ไม่ได้จัดการกรณี []
    • เป็น partial function ที่ไม่ได้ถูกนิยามสำหรับอินพุตที่เป็นไปได้ทั้งหมด

สองวิธีในการเปลี่ยน partial function ให้เป็น total function

  • ทำให้ชนิดข้อมูลของค่าที่ส่งคืนอ่อนลง

    • หากเปลี่ยนเป็น head :: [a] -> Maybe a ก็สามารถคืนค่า Nothing เมื่อลิสต์ว่างได้ จึงกลายเป็น total function
    • การ implement ทำได้ง่ายขึ้น แต่ผู้เรียกต้องจัดการความเป็นไปได้ของ Nothing เสมอ
    • มีตัวอย่างที่แม้อ่านตัวแปรสภาพแวดล้อม CONFIG_DIRS แล้วตรวจไปแล้วว่าลิสต์ไม่ว่าง แต่ใน main ก็ยังต้องจัดการกิ่ง Nothing ของผลลัพธ์จาก head อีกครั้ง
    • การตรวจซ้ำทำให้โค้ดรก และในกรณีซับซ้อนอาจสะสมเป็นต้นทุนด้านประสิทธิภาพได้ด้วย
    • แม้การตรวจด้านหน้าจะถูกลบออก ข้อผิดพลาดด้านหลังที่ “ไม่มีทางเกิดขึ้น” ก็ไม่ถูกแสดงออกผ่านชนิดข้อมูล
    • สุดท้ายระบบชนิดข้อมูลจึงเกิดช่องโหว่ และต้องพึ่งพาการทดสอบหรือการตรวจทานด้วยมือเพื่อค้นพบบั๊ก
  • ทำให้ชนิดข้อมูลของอาร์กิวเมนต์แข็งแรงขึ้น

    • หากไม่ทำให้ชนิดข้อมูลของค่าที่ส่งคืนอ่อนลง แต่ทำให้ชนิดข้อมูลของอาร์กิวเมนต์แข็งแรงขึ้น ก็สามารถตัดความเป็นไปได้ที่ head จะถูกเรียกกับลิสต์ว่างออกไปได้
    • NonEmpty a ของ Data.List.NonEmpty ใช้แทนลิสต์ที่ไม่ว่าง
    • นิยามคือ data NonEmpty a = a :| [a]
    • แยกสมาชิกตัวแรก a กับลิสต์ที่เหลือ [a] ออกจากกัน ทำให้แม้ tail จะว่าง สมาชิกตัวแรกก็มีอยู่เสมอ
    • head :: NonEmpty a -> a implement ได้ด้วย pattern เดียว และกลายเป็น total function
    • หากเปลี่ยนชนิดข้อมูลที่ส่งคืนเป็น getConfigurationDirectories :: IO (NonEmpty FilePath) ข้อเท็จจริงว่าไม่ว่างจะถูกเก็บไว้ในชนิดข้อมูล
    • nonEmpty :: [a] -> Maybe (NonEmpty a) แปลงลิสต์ทั่วไปเป็น NonEmpty
    • การจัดการ Nothing จะทำเพียงครั้งเดียวที่ขอบเขตของอินพุต
    • ใน main สามารถใช้ได้โดยไม่ต้องมีกิ่งเงื่อนไขซ้ำ เช่น initializeCache (head configDirs)
    • หากภายหลัง getConfigurationDirectories เปลี่ยนไปจนไม่รับประกันว่าไม่ว่างแล้ว ชนิดข้อมูลที่ส่งคืนก็ต้องเปลี่ยนตาม และ main จะไม่ผ่านการตรวจชนิดข้อมูล

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

  • validateNonEmpty :: [a] -> IO () และ parseNonEmpty :: [a] -> IO (NonEmpty a) ต่างก็ตรวจว่าลิสต์ว่างหรือไม่ และเมื่อไม่ผ่านก็ทำให้เกิดข้อผิดพลาด
  • ความต่างอยู่ที่ชนิดข้อมูลของค่าที่ส่งคืน
    • validateNonEmpty คืนค่า () ที่ไม่มีข้อมูล จึงทิ้งผลของการตรวจไป
    • parseNonEmpty คืนค่า NonEmpty a จึงเหลือความรู้ที่ได้จากการตรวจไว้ในระบบชนิดข้อมูล
  • parser มองได้ว่าเป็นฟังก์ชันที่กินอินพุตซึ่งมีโครงสร้างน้อยกว่า แล้วสร้างเอาต์พุตที่มีโครงสร้างมากกว่า
  • ตามนิยามนี้ parseNonEmpty คือ parser แบบง่ายที่พาร์สลิสต์ให้เป็นลิสต์ที่ไม่ว่าง
  • การพาร์สช่วยให้ตรวจสอบที่ขอบเขตระหว่างโปรแกรมกับโลกภายนอกให้เสร็จก่อน แล้วไม่ต้องตรวจแบบเดียวกันซ้ำในภายหลัง

ขอบเขตการพาร์สในระบบนิเวศ Haskell

  • แอปพลิเคชัน Haskell ใช้ parser หลายประเภท ณ จุดที่สัมผัสกับโลกภายนอก
    • aeson: มีชนิดข้อมูล Parser สำหรับพาร์สข้อมูล JSON ให้เป็นชนิดข้อมูลของโดเมน
    • optparse-applicative: มี parser combinator สำหรับอาร์กิวเมนต์บรรทัดคำสั่ง
    • persistent, postgresql-simple: มีกลไกสำหรับพาร์สค่าจากแหล่งเก็บข้อมูลภายนอก
    • servant: พาร์สชนิดข้อมูลของ Haskell จากองค์ประกอบของ path, query parameter, HTTP header เป็นต้น
  • โลกภายนอกสื่อสารด้วย byte stream ไม่ใช่ product type และ sum type ดังนั้นจึงหลีกเลี่ยงการพาร์สไม่ได้
  • หากพาร์สไว้ด้านหน้าก่อนใช้ข้อมูล ก็หลีกเลี่ยงบั๊กได้หลายประเภท และบางประเภทอาจนำไปสู่ช่องโหว่ด้านความปลอดภัยได้
  • หากต้องพาร์สทุกอย่างไว้ล่วงหน้า อาจต้องพาร์สค่าต่าง ๆ เร็วกว่าจุดที่นำไปใช้จริงมาก
  • ในระบบชนิดข้อมูลแบบสแตติก เมื่อ logic การพาร์สกับ logic การประมวลผลไม่สอดคล้องกัน โปรแกรมจะคอมไพล์ไม่ผ่าน

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

  • การตรวจสอบเฉพาะหน้าอาจนำไปสู่ shotgun parsing ตามที่กล่าวกันในสาขา language-theoretic security
  • ในบทความปี 2016 The Seven Turrets of Babel: A Taxonomy of LangSec Errors and How to Expunge Them shotgun parsing คือ anti-pattern ที่โค้ดพาร์สและโค้ดตรวจสอบอินพุตถูกปะปนและกระจัดกระจายอยู่ในโค้ดประมวลผล
  • หากไม่ได้พาร์สอินพุตทั้งหมดไว้ด้านหน้า โปรแกรมอาจประมวลผลอินพุตบางส่วนที่ถูกต้องไปแล้ว ก่อนจะพบข้อผิดพลาดของส่วนอื่นในภายหลัง
    • ในกรณีนี้ต้องย้อนคืนการเปลี่ยนแปลงสถานะที่ทำไปแล้ว
    • บางกรณีอาจ rollback ได้ เช่น transaction ของ RDBMS แต่โดยทั่วไปไม่ได้ทำได้เสมอไป
  • แนวทางที่อิงการตรวจสอบความถูกต้องทำให้ยากหรือเป็นไปไม่ได้ที่จะยืนยันว่าการตรวจสอบทั้งหมดเสร็จสิ้นจริง ๆ ตั้งแต่ด้านหน้า
  • การพาร์สแบ่งโปรแกรมออกเป็น ขั้นตอนการพาร์ส และ ขั้นตอนการรัน ทำให้ความล้มเหลวจากอินพุตที่ไม่ถูกต้องถูกจำกัดไว้ที่ขั้นตอนแรก

วิธีประยุกต์ใช้ในงานจริง

  • ออกแบบโดยเขียนรูปแทนข้อมูลที่ฟังก์ชันต้องการไว้ในซิกเนเจอร์ของชนิดข้อมูลก่อน แล้วค่อยเติมช่องว่างระหว่างรูปแทนที่มีอยู่ในปัจจุบันกับรูปแทนที่ต้องการ
  • หากฟังก์ชันรับลิสต์ [(k, v)] ที่ห้ามมี key ซ้ำ การตรวจแยกต่างหากอย่าง checkNoDuplicateKeys :: ... => [(k, v)] -> m () อาจถูกตกหล่นได้ง่าย
  • วิธีที่ดีกว่าคือให้ฟังก์ชันรับ Map ซึ่งเชิงโครงสร้างไม่อนุญาตให้มี key ซ้ำ
    • จุดที่เรียกใช้อาจไม่ผ่านการตรวจชนิดข้อมูล
    • งานแปลงลิสต์เป็น Map จะถูกดันขึ้นไปตาม call chain
    • เมื่อไปถึงตำแหน่งที่ค่าสร้างขึ้น หรือจุดที่ควรอนุญาตให้มีค่าซ้ำได้จริง จึงใส่การตรวจในรูป [(k, v)] -> m (Map k v)
  • ณ จุดนี้ไม่สามารถละเว้นการตรวจได้ เพราะผลของการตรวจจำเป็นต่อการรันภายหลัง
  • หลักการสองข้อจะเกิดซ้ำ
    • ใช้โครงสร้างข้อมูลที่ทำให้ สถานะที่เป็นไปไม่ได้ไม่สามารถถูกแทนค่าได้
    • ดันภาระการพิสูจน์ขึ้นไปให้ไกลที่สุดเท่าที่ทำได้ แต่อย่าดันไกลเกินกว่าจุดที่จำเป็น

แนวทางการออกแบบเพิ่มเติมและข้อจำกัด

  • ควรให้ชนิดข้อมูลนำทางโค้ด และหลีกเลี่ยงแรงล่อใจที่จะใส่ Bool ลงใน record เพียงเพราะฟังก์ชันที่กำลังเขียนอยู่ตอนนี้
  • ฟังก์ชันที่คืนค่า m () ควรถูกมองอย่างระมัดระวัง
    • อาจจำเป็นเมื่อทำเฉพาะ imperative effect และไม่มีผลลัพธ์ที่มีความหมาย
    • แต่ถ้าเป้าหมายหลักคือการทำให้เกิดข้อผิดพลาด ก็มีความเป็นไปได้สูงว่าจะมีวิธีที่ดีกว่า
  • ไม่จำเป็นต้องกลัวการแบ่งพาร์สข้อมูลเป็นหลายครั้ง
    • การหลีกเลี่ยง shotgun parsing หมายถึงอย่ากระทำการใด ๆ กับข้อมูลอินพุตก่อนพาร์สให้สมบูรณ์
    • ยังสามารถใช้อินพุตบางส่วนเพื่อตัดสินใจว่าจะพาร์สอินพุตอื่นอย่างไรได้
  • ควรหลีกเลี่ยงรูปแทนข้อมูลที่ denormalized โดยเฉพาะเมื่อเปลี่ยนแปลงได้
    • หากคัดลอกข้อมูลเดียวกันไว้หลายที่ ก็จะสามารถแทนสถานะที่ไม่สอดคล้องกันได้ง่าย
    • หากจำเป็นต้อง denormalize จริง ๆ ควรซ่อนไว้หลังขอบเขต abstraction และให้โมดูลที่เชื่อถือได้ขนาดเล็กรับผิดชอบการซิงก์เท่านั้น
  • เมื่อใช้เครื่องมือของ Haskell เพียงอย่างเดียวแล้วยากที่จะแทนเงื่อนไขคงสภาพบางอย่างได้จริง สามารถใช้ newtype แบบนามธรรมและ smart constructor เพื่อทำให้ validator ทำงานเหมือน parser ได้
  • ไม่จำเป็นต้องนำ singletons มาใช้และ refactor ทั้งแอปพลิเคชันเพื่อกำจัด error "impossible" ทั้งหมด แต่ในกรณีเช่นนั้นควรจัดการอย่างระมัดระวัง เช่น ทิ้งเงื่อนไขคงสภาพไว้ในคอมเมนต์

อ่านเพิ่มเติมและข้อควรระวังเชิงปฏิบัติ

  • การใช้ระบบชนิดข้อมูลของ Haskell ให้ดีไม่จำเป็นต้องมี PhD หรือใช้ extension ภาษา GHC รุ่นล่าสุดเสมอไป
  • จุดเริ่มต้นใกล้เคียงกับหลักการง่าย ๆ ว่า “จงเขียน total function” แต่ขั้นตอนการนำไปใช้กับโค้ดจริงอาจไม่ง่าย
  • เนื่องจากชุมชน Haskell มีขนาดเล็ก design pattern และเทคนิคต่าง ๆ บางครั้งจึงเหลืออยู่ในรูปความรู้บอกต่อมากกว่าเอกสาร
  • เอกสารที่เกี่ยวข้องคือ Type Safety Back and Forth ของ Matt Parson
  • สำหรับหัวข้อที่ก้าวหน้ากว่านั้น บทความปี 2018 Ghosts of Departed Proofs ของ Matt Noonan กล่าวถึงเทคนิคการใส่เงื่อนไขคงสภาพที่ซับซ้อนกว่าไว้ในระบบชนิดข้อมูล
  • ในโปรแกรมจริง อาจเป็นเรื่องยากที่จะใส่เงื่อนไขคงสภาพบางอย่างไว้ในระบบชนิดข้อมูล และหลักการเหล่านี้จึงใกล้เคียงกับอุดมคติที่ควรมุ่งไป มากกว่าข้อกำหนดที่ต้องทำอย่างเคร่งครัด

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

 
GN⁺ 2024-07-23
ความคิดเห็นจาก Hacker News
  • เป็นคำแนะนำที่ดีมากและเป็นบทความที่ยอดเยี่ยม มีเหตุผลที่บทความนี้ถูกนำกลับมาโพสต์ซ้ำในไซต์นี้เป็นครั้งคราว
    แนวคิดนี้ ก้าวข้าม paradigm แม้สำหรับคนที่ไม่ได้ใช้ภาษา functional แบบ static type ก็ตาม ในวรรณกรรม object-oriented ยุค 80–90 เช่น Design by Contract ก็เห็นแนวคิดที่คล้ายกันมากได้ และน่าจะย้อนกลับไปเจอบทความวิชาการ การถกเถียง และสเปกที่เก่ากว่านั้นได้อีก
    ผมมองว่า TypeScript ก็มักถูกเขียนในลักษณะค่อย ๆ narrow type ตอน runtime เช่นกัน Design by Contract ก็น่าจะมีอิทธิพลต่อ spec ของ Clojure ซึ่งเป็น dynamic language ด้วย
    โดยพื้นฐานแล้วนี่เป็นเรื่องของ สมมติฐานและการรับประกัน ถ้าสามารถตรวจสอบสมมติฐานบางอย่างแล้วสร้างการรับประกันขึ้นมาได้ ส่วนอื่นของโปรแกรมก็ไม่จำเป็นต้องตรวจสอบสมมติฐานเดียวกันซ้ำอีก
    เวลาการอ่านโค้ดแล้วเห็นว่ามีการตรวจสอบคุณสมบัติที่รับประกันแล้วซ้ำที่อื่น เป็นสิ่งที่ทำให้สับสนที่สุด ทำให้การอนุมานและการปรับปรุงยากขึ้น

    • “คุณสมบัติที่รับประกันแล้ว” นั้นอาจ หายไปได้ ในบางจังหวะ พูดให้แม่นกว่านั้นคือ ขั้นตอนที่ implement และ execute การรับประกันนั้น อาจไม่ทำหน้าที่ของมันอีกต่อไปด้วยเหตุผลใดก็ตาม
      ในเชิงสถิติ เรื่องแบบนั้นย่อมเกิดขึ้นในที่สุด และเมื่อถึงตอนนั้น process, script, code อื่น ๆ ที่พึ่งพาขั้นตอนตรวจสอบ “เดิม” จะลำบากมาก
    • ในภาษาที่มี type system แข็งแรง สิ่งนี้จะกลายเป็นหนึ่งใน ข้อดีเชิงปฏิบัติที่ให้เสรีภาพ เมื่อโปรแกรมใหญ่และซับซ้อนขึ้น
      แต่ต้องใช้งานมันจริง ๆ เช่นมีคลาส UncheckedEmail, ValidEmail, VerifiedEmail และบังคับว่าการเปลี่ยนจากขั้นหนึ่งไปอีกขั้นต้องผ่านกระบวนการตรวจสอบอีเมลเสมอ
      แบบนี้ก็ไม่ต้องเดาว่าอีเมลแอดเดรสนั้นยังไม่ได้ตรวจสอบ ถูกต้องตามรูปแบบ หรือผ่านการยืนยันแล้ว และไม่ต้องมี boolean อย่าง is_email_verified ที่อาจลืมอัปเดตหรือตรวจสอบ ถ้าใส่ค่าผิดที่ผิดทาง type checker ก็จะร้องเตือน และคนก็ไปโฟกัสเรื่องสำคัญได้
    • หลังจากไล่อ่านคอมเมนต์ของบทความเก่า ๆ แล้ว หนึ่งในปัญหาใหญ่ที่สุดของบทความนี้น่าจะเป็น ชื่อเรื่อง ชื่อเรื่องทำตัวเหมือนสมอ ทำให้หลายคนโต้แย้งสิ่งที่ไม่มีอยู่ในเนื้อหา แต่มีเพียงชื่อเรื่องที่บอกเป็นนัยแบบไร้บริบท
      ดังนั้นจึงมีกรณีที่รับสารเหมือนผู้เขียนบอกว่าไม่ต้องทำ validation เลย ให้ parse อย่างเดียว แต่บทความจริง ๆ พูดถึงว่า จะ validate ข้อมูลที่ไหน และจะทำอะไรกับผลลัพธ์นั้น ไม่ใช่บทความที่บอกให้เลิก validation ทั้งหมด
  • ถึงจะเป็นบทความปี 2019 แต่ก็ยังเป็นคำแนะนำที่ค่อนข้างดีอยู่ pattern นี้เข้ากับ C# สมัยใหม่ได้ดีมาก และยังประหยัดพื้นที่ได้เพราะละการประกาศตัวแปรแบบชัดเจนได้
    if(!Whatever.TryParse(input, out var output)) output = some-sane-default;
    หรือ
    if(!Whatever.TryParse(input, out var output)) throw new ApplicationException($"Not a valid Thingy: {input}");
    เคล็ดลับสำหรับมือโปร: แบบหลังอย่าทำใน kernel mode driver

    • เคล็ดลับสำหรับมือโปร: อย่าทำทั้งสองแบบ โดยเฉพาะแบบแรก ห้ามทำเด็ดขาด
      การจัดการแบบชัดเจนย่อมดีกว่า ค่า default โดยนัย ที่ถูกนำมาใช้แทนเมื่อค่าที่คิดว่าถูกต้องนั้นผิด
      สิ่งที่ควรทำคือยกธงตั้งแต่ต้นว่า parse ล้มเหลว แล้วกำหนดกระบวนการและโปรโตคอลสำหรับจัดการไฟล์ที่โหลดไม่ได้ให้ชัดเจนมาก ๆ แบบนั้นจะบังคับให้คุณถามคำถามยาก ๆ ที่สองทางเลือกข้างบนไม่ได้จัดการ
      ปัญหาจริงของ kernel mode driver ของ CrowdStrike ล่าสุดที่ parse ไฟล์ def/config บางอย่างล้มเหลว คือ developer, product owner, business analyst ไม่ได้ถามว่า “ถ้าพยายามโหลดไฟล์ที่ไม่ valid จะเกิดอะไรขึ้น?”
    • ทำไมถึงบอกว่าแค่ “ค่อนข้างดี”? แล้วมันเกี่ยวอะไรกับปีที่บทความเผยแพร่? หมายความว่าถ้าเผยแพร่ก่อนปี 2019 คำแนะนำในบทความจะมีอำนาจน่าเชื่อถือมากกว่านี้หรือ?
    • ผมไม่อยากให้ใช้วิธีแรก ควรจัดการเคสที่ไม่ดี การ fallback ไปใช้ “ค่า default ที่สมเหตุสมผล” ควรเกิดขึ้น น้อยมาก ๆ
      การจัดการแบบชัดเจน > การจัดการแบบโดยนัย
    • if(!Whatever.TryParse(input, out var output)) output = some-sane-default;
      ผมเกลียดวิธีนี้จริง ๆ ผมคิดว่าข้อผิดพลาดจาก input ที่ไม่ valid ควรถูกจัดการนอกฟังก์ชัน parse ใน F# ทำแบบนั้นได้ง่าย
      type Whatever =
      static member create input =
      match input with
      | ValidWhatever x -> Some x
      | _ -> None
      match Whatever.create input with
      | Some x -> // จัดการข้อมูลที่ parse แล้ว
      | None -> // จัดการกรณีที่ parse ไม่สำเร็จอย่างถูกต้อง
      หรือจะใช้ Option.map/Option.bind เพื่อทำให้ pipeline สำหรับงานต่อเนื่องสะดวกขึ้นก็ได้
      แบบนี้จะสร้าง instance ได้ผ่านเมธอด create ที่ parse input เท่านั้น
      แต่ในทางปฏิบัติจริง ๆ มีโอกาสสูงว่าจะอยากใช้ result มากกว่า option แม้นั่นจะเป็นเรื่องรองก็ตาม
    • แทบจะนึกสถานการณ์ที่อยากเห็นโค้ดแบบ if(!Whatever.TryParse(input, out var output)) output = some-sane-default; ไม่ออกเลย หรืออาจไม่มีเลยด้วยซ้ำ
      ถ้า ไม่ได้ให้ input มาเลย กล่าวคือ parameter เป็น optional การใช้ค่า default ที่สมเหตุสมผลก็เข้าท่า
      แต่ถ้ามีการให้ input ที่ผิด มา ก็ไม่ควรทำเหมือนไม่มีปัญหาอะไร
      ถ้ามีคนเดินเข้าร้านดอกไม้แล้วขอกาแฟ คำตอบที่ถูกต้องไม่ใช่ยื่นดอกกุหลาบให้ ถ้าคนนั้นพยายามดื่มมัน ปากคงฉีกหมด
      สำหรับชุด input นั้น method, module, program ไม่มี output ที่นิยามไว้ ควรทำให้ข้อเท็จจริงนั้นปรากฏชัด แทนที่จะทำสิ่งที่ผิดหรือคลุมเครืออย่างเงียบ ๆ จนทำให้โปรแกรมกลายเป็นสิ่งที่อนุมานไม่ได้อย่างรวดเร็ว อย่าปล่อยให้มันถูกจับเป็นบั๊กพฤติกรรมประหลาดในอีกหลายเดือนให้หลัง แต่ควรทำให้ปัญหาเกิดขึ้นอย่างชัดเจน และทิ้ง stack trace ที่นำตรงไปยังจุดปัญหาไว้ แบบนั้นดีกับตัวเองด้วย
  • เป็นคำแนะนำให้ใช้ระบบชนิดข้อมูลที่แข็งแรง เพื่อทำให้ ไม่สามารถแทนสถานะข้อผิดพลาดได้ ซึ่งดีมากสำหรับการลดบั๊กในซอฟต์แวร์โดยรวม
    การคิดให้ลึกขึ้นเกี่ยวกับปัญหาและออกแบบแบบนี้ต้องใช้เวลามากขึ้น แต่ในหลายกรณี เวลานั้นก็คุ้มค่าอย่างเต็มที่

    • ถ้าเป็นภาษาที่รองรับชนิดข้อมูลเชิงพีชคณิต ผมกล้าพูดเลยว่าวิธีนี้ไม่ได้ใช้เวลามากขึ้น มันจะเกิดขึ้นอย่างเป็นธรรมชาติ
      แน่นอนว่าถ้าเป็นภาษาอย่าง C++, Java, C#, Python, Go, JavaScript ซึ่งเป็นภาษาที่ต้องใช้ ขั้นตอนแบบตั้งใจจำนวนมากในการสร้างโมเดลข้อมูล ก็จะใช้เวลามากขึ้น
  • “ตอนนี้ผมมีสโลแกนสั้น ๆ แต่ทรงพลังที่สื่อว่าการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูลมีความหมายอย่างไรสำหรับผม และที่ดีกว่านั้นคือมันมีแค่สามคำ: Parse, don’t validate.”
    สโลแกนของผมกลับใกล้เคียงกับ จงตรวจสอบความถูกต้องเฉพาะใน constructor เดียวเสมอ มากกว่า จะเป็นฟังก์ชัน constructor ก็ได้
    ทำแบบนั้นแล้วออบเจ็กต์ที่ไม่ถูกต้องจะไม่มีทางมีอยู่ตั้งแต่แรก และจะมีแหล่งความจริงเดียวเสมอ ถ้าต้องการแก้ไขออบเจ็กต์ ก็สามารถ implement โดยเรียก constructor เดิมอีกครั้งเพื่อสร้างสถานะใหม่ได้

    • ไม่ใช่เรื่องเดียวกัน
      ประเด็นหลักคือถ้าแค่ตรวจสอบความถูกต้อง ข้อมูลนั้นจะ หายไป ในภายหลัง
      เช่น การตรวจสอบว่า int ค่าหนึ่งเป็นบวกหรือไม่นั้นให้ประโยชน์จำกัด เพราะถ้าไม่ parse ค่านั้นให้เป็นจำนวนเต็มบวก ข้อมูลดังกล่าวจะไม่เหลืออยู่ในระดับ type ในภายหลัง อาร์เรย์หรือลิสต์ที่ไม่ว่างก็เช่นกัน consumer ถัดไปอาจต้องตรวจสอบซ้ำว่าลิสต์นั้นไม่ว่างจริงหรือไม่
      ข้อมูลประเภทนี้ไม่สามารถเข้ารหัสไว้ในออบเจ็กต์หรือ constructor ได้เสมอไป
  • แหล่งข้อมูลที่เกี่ยวข้อง: Making Impossible States Impossible ของ Richard Feldman
    https://www.youtube.com/watch?v=IcgmSRJHu_8

  • ก่อนหน้านี้ก็เคยมีการถกเถียงที่ดี ๆ อยู่
    https://news.ycombinator.com/item?id=35053118
    https://news.ycombinator.com/item?id=21476261

  • ทุกครั้งที่หัวข้อนี้ถูกยกขึ้นมา ผมนึกถึงข้อ 5 ของ https://cr.yp.to/qmail/guarantee.html ที่นั่นมีประโยคอย่าง “อย่า parse” และ “ในโลกคอมพิวติ้ง อินเทอร์เฟซคำสั่งมีอยู่สองแบบ: อินเทอร์เฟซที่ดี และ user interface”
    ถ้ามีคลาสที่สอน การเขียนโปรแกรมระดับกลาง ไม่ใช่ขนาดเล็กหรือขนาดใหญ่ ผมอยากมอบหมายให้นักศึกษาเขียนเรียงความเปรียบเทียบและชี้ความแตกต่างของข้อเสนอเหล่านี้ แต่ละข้อมีสิ่งให้เรียนรู้ และอาจไม่ได้ขัดแย้งกันเท่าที่เห็นในครั้งแรก

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

  • เป็นหนึ่งในบทความที่ผมชอบที่สุดในบรรดาที่อ่านมาตลอดอาชีพ ผมเห็นบ่อยว่าผู้คนอ่านแค่ชื่อเรื่องแล้วสรุปว่า parsing กับ validation somehow เป็นสิ่งที่แยกกันโดยสิ้นเชิง แต่จริง ๆ แล้วไม่ใช่แบบนั้น การ parse มักรวมการตรวจสอบความถูกต้องไว้ด้วย
    เนื้อหานี้ถูกพูดถึงในส่วน “Use abstract datatypes to make validators ‘look like’ parsers” ของบทความ
    อยู่ในขอบเขตเดียวกับแนวคิดที่ว่าให้หลีกเลี่ยงการยึดติดกับ primitive type