- จากประสบการณ์เขียนซอฟต์แวร์มากว่า 20 ปี การพิมพ์แบบสแตติกที่เข้มงวด แทบจะคุ้มค่าแก่การเลือกเสมอ ยกเว้นกรณีอย่าง REPL หรือสคริปต์ใช้ครั้งเดียว
- ชนิดข้อมูลคือ สัญญา ที่ถูกทิ้งไว้ในโค้ดระหว่างผู้เรียกกับสิ่งที่ถูกเรียก ช่วยกรองพารามิเตอร์หรือค่าที่คืนกลับมาผิดพลาดได้ตั้งแต่ตอนคอมไพล์หรือตรวจชนิดข้อมูล
- ตัวอย่างสตริง
"20"จากอินพุต HTML ที่ถูกใช้ราวกับเป็นตัวเลขจนกลายเป็น"201"แสดงให้เห็นความต่างระหว่างข้อผิดพลาดที่จับได้ก่อนรันไทม์กับข้อผิดพลาดที่หลุดไปถึงลูกค้า - Svix พยายามใส่ Redis key, ค่าแคช, ตัวระบุอย่าง
PersonId·PetIdและการตรวจสอบอินพุต API ไว้ใน ระบบชนิดข้อมูล เพื่อลดการพิมพ์ผิดและการส่ง ID ผิดประเภท - หากละชนิดข้อมูลไป การทำเวอร์ชันแรกอาจเร็วขึ้น แต่ต้นทุนด้านเอกสาร การทดสอบ และการดีบักจะสูงขึ้น และเมื่อใช้ type inference กับการรองรับจาก IDE ก็จะรีแฟกเตอร์และออนบอร์ดได้ง่ายขึ้น
เหตุผลที่ยืนกรานใช้ static type
- การพิมพ์แบบสแตติกที่เข้มงวดไม่ใช่แค่ไอเดียที่ดี แต่ใกล้เคียงกับ ค่าปริยายที่ถูกต้อง สำหรับซอฟต์แวร์ส่วนใหญ่
- ภาษาที่ไม่มีชนิดข้อมูลหรือรูปแบบผสมก็ยังมีประโยชน์
- การใช้ REPL
- สคริปต์ใช้ครั้งเดียวในสภาพแวดล้อมที่แทบไม่มี type อยู่แล้ว เช่น เชลล์
- นอกเหนือจากนั้น ส่วนใหญ่จะชอบ type ที่เข้มงวด
- ถ้าไม่ใช้ type ความเร็วในการพัฒนาอาจเร็วขึ้นในทันที แต่ผู้เขียนมองว่าใกล้เคียงกับ “การพุ่งเต็มสปีดเข้าหาหน้าผา”
- สุดท้ายแล้วมีทางเลือกอยู่สองแบบ
- ทำงานเพิ่มเพื่อให้ตรวจสอบเงื่อนไขคงที่ได้ตั้งแต่ตอนคอมไพล์หรือตรวจชนิดข้อมูล
- ทำงานน้อยลงแล้วไปตรวจตอนรันไทม์ หรือไม่ก็ไม่ตรวจแม้แต่ตอนรันไทม์
- ข้อผิดพลาดตอนรันไทม์ไม่ได้ถูกจับได้เสมอระหว่างพัฒนา และต่อให้จับได้ก็อาจเกิดในแบบที่ผู้ใช้มองเห็น
- การทดสอบช่วยได้ แต่ยากที่จะทดสอบชนิดพารามิเตอร์ฟังก์ชันที่ผิดทุกแบบ และการใช้ type เพื่อกันชนิดข้อมูลผิดตั้งแต่แรกนั้นง่ายกว่า
Type เชื่อมตรงกับสัญญาในโค้ดและการลดบั๊ก
- Type เป็นทั้ง คำอธิบายในโค้ด ที่มีประโยชน์ต่อทั้งคนและเครื่องมือ และเป็นกลไกที่ทำให้สัญญาระหว่างชิ้นส่วนของโค้ดเข้มงวดยิ่งขึ้น
- แม้จะเป็นฟังก์ชันอวยพรวันเกิดเหมือนกัน แต่ความชัดเจนของสัญญาแตกต่างกันมาก
birthdayGreeting1(...params)ไม่ได้บอกแม้กระทั่งจำนวนพารามิเตอร์ จึงยากจะรู้การทำงานหากไม่อ่านเอกสารbirthdayGreeting2(name, age)บอกใบ้ว่ามีชื่อและอายุ แต่ไม่มี typebirthdayGreeting3(name: string, age: number): stringใส่ชนิดข้อมูลของอินพุตและค่าที่คืนกลับมาไว้ในสัญญาด้วย
- ถ้าฟังก์ชันถูกเปลี่ยนให้ใช้
age + 1เวอร์ชันที่ไม่มี type จะมีปัญหาเมื่อรับสตริง- ค่าที่มาจากอินพุต HTML อาจเป็นสตริงเสมอ
birthdayGreeting2("John", "20")จะคืนค่า"John will turn 201 next year!"- เวอร์ชันที่มี type กำหนดให้
ageต้องเป็นตัวเลข ดังนั้นการเรียกใช้ผิดจะคอมไพล์ไม่ผ่าน
- สัญญาระหว่างผู้เรียกกับสิ่งที่ถูกเรียกยิ่งสำคัญขึ้นเมื่อโค้ดเบสใหญ่ขึ้น
- เมื่อสิ่งที่ถูกเรียกเปลี่ยนไป ก็รู้ได้ว่าผู้เรียกจะได้รับผลกระทบอย่างไร
- สำคัญเป็นพิเศษเมื่อผู้เรียกกับสิ่งที่ถูกเรียกถูกเขียนโดยคนละคน เช่น ไลบรารีโอเพนซอร์ส
- หากไม่มีสัญญาแบบนี้ ก็จะยากที่จะรู้ว่าการเปลี่ยนแปลงส่งผลไปไกลแค่ไหน
ข้อดีต่อประสบการณ์พัฒนา การรีแฟกเตอร์ และการออนบอร์ด
- ข้อมูล type ถูกนำไปใช้โดย IDE และเครื่องมือพัฒนา เพื่อยกระดับประสบการณ์พัฒนาอย่างมาก
- ระหว่างเขียนโค้ด หากความคาดหวังผิดก็รู้ได้ทันที จึงลด ภาระทางความคิด
- นักพัฒนาไม่จำเป็นต้องจำ type ของตัวแปรและฟังก์ชันทั้งหมดในบริบทปัจจุบัน เพราะคอมไพเลอร์จะบอกจุดที่ไม่สอดคล้องให้
- การรีแฟกเตอร์ก็ง่ายขึ้น
- เมื่อต้องเปลี่ยน implementation ของฟังก์ชัน คอมไพเลอร์ช่วยบอกได้ว่าทำลายสมมติฐานของส่วนอื่นหรือไม่
- วิศวกรใหม่ก็ปรับตัวเข้ากับโค้ดเบสหรือไลบรารีได้ง่ายขึ้น
- สามารถไล่ตาม type definition เพื่อเข้าใจว่าถูกใช้งานที่ไหนบ้าง
- และเพราะถ้าแก้แล้วจะเกิด compile error จึงทดลองได้ง่าย
- ความแตกต่างนี้เห็นได้จากตัวอย่างฟังก์ชันที่รับ
PersontypebirthdayGreeting3(person: Person)ทำให้หา usage ของPersonผ่าน IDE ได้ง่าย- ส่วน
birthdayGreeting2(person)ที่ไม่มี type ต้องอ่านโค้ดทั้งโค้ดเบสจึงจะรู้ว่าแท้จริงแล้วคาดหวังPerson
- เอกสารช่วยอุดช่องว่างได้บางส่วน แต่เอกสารมักล้าสมัย และ type ก็เป็นเอกสารที่อยู่ในตัวโค้ดเอง
- ผู้เขียนมองว่า type คล้ายกับชื่อตัวแปรที่มีประโยชน์ในเวอร์ชันที่ทรงพลังยิ่งกว่า
วิธีที่ Svix ใส่ข้อมูลลงในระบบชนิดข้อมูล
- Svix พยายามใส่ข้อมูลให้มากที่สุดเท่าที่ทำได้ลงใน ระบบชนิดข้อมูล เพื่อลดข้อผิดพลาดที่จับได้ตั้งแต่ตอนคอมไพล์ และปรับปรุงประสบการณ์พัฒนาไปพร้อมกัน
- Redis เป็นโปรโตคอลที่อิงสตริงโดยเนื้อแท้และไม่มี type ในตัว จึงอาจทำให้ข้อดีของ type หายไปในชั้น Redis
- ตัวอย่างแคชแบบง่ายมีบั๊กอยู่สองอย่าง
- มีการพิมพ์ชื่อคีย์ผิด เช่น
person-{id}กับpreson-{id} - พยายามโหลดข้อมูลคนด้วย type
Pet
- มีการพิมพ์ชื่อคีย์ผิด เช่น
- เพื่อหลีกเลี่ยงปัญหาเหล่านี้ Svix ใช้สองแนวทาง
- บังคับให้คีย์ต้องเป็น type เฉพาะ แทนที่จะเป็นสตริงทั่วไป
- จับคู่คีย์กับค่าแบบบังคับ
- ตัวอย่างเช่น ถ้าใช้คีย์ที่สร้างด้วย
PersonCacheKey::new(id)โค้ดที่พยายามรับผลลัพธ์จากcache.get(PersonCacheKey::new(id))เป็นPetจะคอมไพล์ไม่ผ่าน - แม้แต่ ID แบบ
Stringธรรมดาก็ชวนให้เกิดความผิดพลาดได้do_something(id: String)ไม่ได้ชัดเจนว่าควรรับ ID แบบไหน- อาจเผลอส่ง
pet.idทั้งที่จริงควรส่งpet.owner
- Svix แยก type ให้แต่ละ ID
PersonId(String)PetId(String)ownerของPetเป็นPersonId
- การตรวจสอบความถูกต้องของ ID ที่รับมาจาก API ก็ผูกกับการสร้าง type ด้วย
- ตัวอย่างเช่น pet ID มีรูปแบบเป็น Ksuid ต่อท้าย prefix
pet_ PetIdจะไม่สามารถถูกสร้างได้หากไม่ผ่านการตรวจสอบ- ด้วยวิธีนี้ เมื่อคืนค่า
404 Not Foundเพราะหาสัตว์เลี้ยงไม่เจอในฐานข้อมูล ก็มั่นใจได้ว่ารูปแบบ ID นั้นถูกต้องแล้ว - ส่วน ID ที่ไม่ถูกต้องจะถูกจัดการเป็น
422หรือ400ตั้งแต่ใน API handler
- ตัวอย่างเช่น pet ID มีรูปแบบเป็น Ksuid ต่อท้าย prefix
ข้อโต้แย้งและบทบาทของเครื่องมือ
- ข้อโต้แย้งหลักต่อ type คือ ความเร็วในการพัฒนา เส้นโค้งการเรียนรู้และความซับซ้อนของ type รวมถึงความพยายามและ boilerplate
- การทำ prototype โดยไม่มี type อาจเร็วกว่าได้จริง
- สามารถคอมเมนต์โค้ดออกได้โดยไม่ต้องเจอคำบ่นจากคอมไพเลอร์
- สามารถใส่ค่าที่ผิดลงในฟิลด์ได้ชั่วคราวจนกว่าจะตัดสินใจได้ว่าค่าที่ถูกต้องคืออะไร
- แต่ผู้เขียนมองว่านี่คือหนี้ทางเทคนิคที่รุนแรงและไม่จำเป็น และจะต้องจ่ายต้นทุนซ้ำหลายครั้งตอนดีบักทั้งในเครื่องตัวเอง ชุดทดสอบ และโปรดักชัน
- แม้จะมีเส้นโค้งการเรียนรู้ แต่คนส่วนใหญ่ไม่จำเป็นต้องเป็นผู้เชี่ยวชาญด้าน type
- แค่การแสดง type แบบง่าย ๆ ก็เพียงพอสำหรับการทำงานได้มากแล้ว
- ถ้าติดขัดค่อยขอความช่วยเหลือ
- นักพัฒนาต้องเรียนรู้อะไรอีกมากอยู่แล้ว ทั้งการเขียนโค้ด เฟรมเวิร์กอย่าง React, Axum เป็นต้น ดังนั้นผู้เขียนจึงมองว่าภาระในการเรียนรู้ type ถูกพูดเกินจริง
- การเรียนรู้ type เป็นต้นทุนครั้งเดียว และข้อดีที่ได้จาก type ระหว่างการออนบอร์ดเข้าสู่โค้ดเบสใดโค้ดเบสหนึ่งมีมากกว่า
- ถ้าไม่ใช้ type ก็ต้องพึ่งเอกสารและการทดสอบจำนวนมากเพื่อให้ได้ความปลอดภัยพื้นฐาน
- เอกสารและการทดสอบอาจล้าสมัยได้
- ผู้เขียนมองว่าการเพิ่ม type ที่ถูกต้องใช้แรงน้อยกว่า
- ในภาษาที่ไม่มี type inference การพิมพ์อาจน่ารำคาญ
- ตัวอย่าง Java ทำให้เกิดการเขียนซ้ำอย่าง
Person person1 = newPerson(); - ภายหลังมีการแก้ไขเพิ่มเติมในบทความว่า Java ก็มี type inference แล้ว
- ตัวอย่าง Java ทำให้เกิดการเขียนซ้ำอย่าง
- ภาษาที่มี type inference อย่าง Rust กระชับกว่า เช่น
let person1 = new_person(); - หากต้องการประโยชน์จาก type อย่างเต็มที่ จำเป็นต้องมีตัวแก้ไขโค้ดหรือ IDE ที่รองรับการเติมโค้ดอัตโนมัติแบบสมัยใหม่และเข้าใจภาษา
- ต่างจากข้อถกเถียงเรื่องรสนิยมอย่าง
vimปะทะemacsหรือแท็บปะทะสเปซ ผู้เขียนมองว่า type ให้ประโยชน์คุ้มต้นทุนมากจนยากจะเข้าใจเหตุผลที่จะไม่ใช้มัน - มีบทความต่อเนื่องคือ using the type system effectively
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
สิ่งที่น่าอึดอัดที่สุดในการถกเถียงนี้คือทั้งหมดเป็นเรื่องว่าผู้คน รู้สึก อย่างไร และขาดหลักฐานเชิงประจักษ์
งานวิจัยเดิมมองว่าไม่มีความแตกต่างอย่างมีนัยสำคัญระหว่างสองแนวทางนี้ และหากไม่มีงานวิจัยใหม่ ก็ยากจะสรุปได้ว่าฝั่งที่แต่ละคนชอบนั้นถูกต้องแน่นอน
โดยส่วนตัวผมชอบภาษาที่มี type แต่ type system แบบ TypeScript ยังไม่พอ เพราะไม่สามารถใช้ type ได้จริงใน runtime ทำให้ยังมีบั๊กใน runtime เหลืออยู่ และ logic ใน runtime จำนวนมากก็ encode เข้าไปใน type system ไม่ได้ จึงยังต้องตรวจกรณีที่เป็นไปไม่ได้เองอยู่ดี
ถ้า type system ทำให้แทบไม่ต้องคิดถึงบั๊กใน runtime ได้ นั่นคงเป็นข้อได้เปรียบมหาศาล แต่ภาษาส่วนใหญ่ยังไม่ถึงระดับนั้น และติดอยู่ตรงกลางที่คลุมเครือระหว่าง overhead กับประโยชน์บางส่วน
เหตุผลที่จำนวนบั๊กหรือความเร็วไม่ได้ต่างกันมาก น่าจะเป็นเพราะสุดท้ายมันหักล้างกันเอง ถ้าไม่มี safety net จาก type ก็จะเขียน test มากขึ้น ในทางกลับกันถ้าเชื่อมั่น type system มากเกินไป สุดท้ายก็จะมีบั๊กใน runtime เหลืออยู่ในปริมาณใกล้เคียงกัน อยากให้มีงานวิจัยที่หนักแน่นในเรื่องนี้ แต่เป็นปัญหาที่ยาก
ประเด็นหลักน่าจะใกล้เคียงกับว่า เหตุผลเชิงอัตวิสัยที่ทำให้ตัดสินว่า type ไม่คุ้มค่าต่อการลงทุน คืออะไร มากกว่านั้น
เมื่อหลายปีก่อนผมเคยดูงานวิจัยเรื่อง productivity ของนักพัฒนา และแทบทั้งหมดแย่มากหรือใช้ได้จริงเฉพาะกับ junior เท่านั้น เช่น มือใหม่ได้ประโยชน์มากจาก feedback ที่รวดเร็วต่อ static error
แทบเป็นไปไม่ได้ที่จะนำการออกแบบการทดลองที่ดีไปใช้กับผู้เชี่ยวชาญแทนนักศึกษา และยังต้องแยกตัวแปรมากมาย เช่น ความแตกต่างรายบุคคล ประเภทของงานพัฒนา วิธีบริหารจัดการ ทำให้ดึงสัญญาณออกมาได้ยาก น่าเศร้า แต่หลายสิ่งในชีวิตวัดผลอย่างมีประสิทธิภาพได้ยาก
บทความและคอมเมนต์จำนวนมากพูดถึงความสะดวกของโปรแกรมเมอร์ productivity และ “ความถูกต้อง” แต่จากงานวิจัยปัจจุบันยังไม่มีผลลัพธ์ที่มีนัยสำคัญว่า static type ทำให้สิ่งเหล่านี้ดีขึ้นหรือแย่ลง โดยพื้นฐานแล้วเป็นเรื่องอัตวิสัย
อย่างไรก็ตาม มีผลอย่างหนึ่งที่ static type ให้ได้จริงและพิสูจน์ได้แบบง่าย ๆ คือช่วยให้เขียนโค้ดที่มีประสิทธิภาพมากขึ้นได้ จุดนี้ควรเป็นศูนย์กลางของการถกเถียงเรื่องวินัยของ type ส่วนที่เหลือในตอนนี้ยังค่อนข้างลอย ๆ
TypeScript ที่กล่าวถึงในบทความจริง ๆ แล้วไม่ใช่ strongly typed แต่เป็น static type ที่ weak type เพราะ type ใกล้เคียงกับ annotation จึงไม่มีการรับประกันด้าน performance หรือ memory layout ดังนั้นนอกจากการใช้เป็นเอกสารแล้ว ก็จ่ายต้นทุนของ static type แต่แทบไม่ได้ประโยชน์จริง
น่าประหลาดใจที่ชุมชนเทคโนโลยีเพิกเฉยต่อหลักฐานจริง และยอมรับความชอบเชิงวัฒนธรรมหรือส่วนบุคคลราวกับเป็นข้อเท็จจริง
เช่นเดียวกับเทคนิคอื่น ๆ มันมีรู ดังนั้นเพื่อให้ได้ความน่าเชื่อถือสูงสุดต้องผสมหลายเทคนิคเข้าด้วยกัน การทิ้ง static type เพียงเพราะมันจับทุกอย่างไม่ได้ ก็คล้ายกับการไม่ล็อกประตูเพราะขโมยอาจทุบหน้าต่างเข้ามาได้ ถ้าความปลอดภัยสำคัญจริง ๆ ก็ควรล็อกประตูและติดเหล็กดัดที่หน้าต่างด้วย ไม่ใช่เลือกแค่อย่างใดอย่างหนึ่ง
ถ้าภาษาทรงพลังพอที่จะเขียนโปรแกรมทั่วไปได้ มันก็ทรงพลังพอที่จะสร้างบั๊กได้เช่นกัน
Static type อาจมีประสิทธิภาพในการจับบั๊กบางชนิด แต่ไม่ใช่ทั้งหมด บางครั้งมันช่วยให้อ่านง่ายขึ้น เหมือน unit test แบบ static หรือภาษาเฉพาะโดเมนสำหรับเอกสารที่รันได้
โดยทั่วไปภาษา dynamic จะคล่องตัวกว่า และเขียน test จำนวนมากได้ง่ายกว่า แม้จะมี test บางส่วนที่ไม่จำเป็นต้องเขียนหากเป็นภาษา static type ดังนั้น type ก็ยังมีประโยชน์ แต่ไม่ได้ทรงพลังครอบจักรวาลเท่าที่มักเชื่อกัน
นอกเหนือจาก แรงกดดันทางสังคม ที่ว่าควรชอบ static type แล้ว เหตุผลที่ท้ายที่สุดผมถอยห่างจาก static type ก็เพราะรอบ ๆ มันมักมีหอคอยงาช้างถูกสร้างขึ้นเสมอ
ผมทำซอฟต์แวร์ในทั้งสองพาราไดม์มาอย่างละ 10 ปี และตอนนี้ชอบทางที่ไม่ใช้ type system มากกว่า
dynamic type สำหรับผมให้ความรู้สึกเหมือนแรงบังคับให้เขียนโค้ดเรียบง่าย คล้ายกับที่ unit test บังคับให้โค้ดประกอบกันได้ หมายถึงโค้ดที่อ่านง่ายและเข้าใจง่าย
ผมก็ไม่ค่อยเห็นด้วยกับข้ออ้างที่ว่ามันทำให้นักพัฒนามือใหม่เข้าถึง codebase ได้ง่ายขึ้น เพราะมันมักส่งเสริมลูปซ้ำ ๆ ของการกำจัดขีดแดงโดยไม่เข้าใจ Type system ทำให้แต่ละโปรเจกต์ต้องเรียนรู้อีกภาษาหนึ่งที่เฉพาะโดเมนมาก ๆ ซ้อนอยู่บนภาษาเดิม และบ่อยครั้งขัดขวางการเข้าใจพฤติกรรมจริง
ปัญหาที่บทความยกมาสามารถแก้ได้ด้วยวิธีที่แข็งแรงพอ ๆ กับ type แต่เข้าใจง่ายกว่า จะใช้ type แบบเรียบง่ายก็ได้ แต่จากประสบการณ์ของผม ในความเป็นจริงแทบไม่เคยเป็นแบบนั้น และผมก็ไม่ชอบ autocomplete ด้วย ดังนั้นก็รับไปตามนั้น
ผมอาจเป็นแค่นักพัฒนาแก่ ๆ ที่ตะโกนว่า “โค้ดคือเอกสาร” แต่ก็อาจเป็นความคิดที่มาจากความไม่พอใจลึก ๆ ต่อเหล่านักพัฒนาในวงการทุกวันนี้ที่ “ChatGPT บอกว่าถูก และได้เงินเดือนสูง” ก็ได้
แต่โดยทั่วไป หลักฐานฝั่งตรงข้ามดูแข็งแรงกว่า type ที่สร้างขึ้นภายหลังเพื่อทำเอกสารให้โค้ด dynamic จริง ๆ มักซับซ้อนกว่าฟังก์ชันเดียวกันที่ implement ด้วย static type ตั้งแต่ต้นมาก ระบบนิเวศ TypeScript อย่าง DefinitelyTyped มีตัวอย่างให้เห็นมากมาย
คงพูดได้ยากว่า type เหล่านั้นถูก “ใช้อย่างเรียบง่าย” แต่ความซับซ้อนนั้นไม่ได้มาจากตัว type system เองหรือวิธีจัดหา type definition แต่มาจาก ความซับซ้อนของโค้ด dynamic ที่มันกำลังอธิบาย
แพ็กเกจที่เทียบเท่ากันซึ่งทำด้วย static type ตั้งแต่ต้นมักมี interface ที่เรียบง่ายกว่า เพราะกำหนดไว้ตั้งแต่ข้างหน้า ไม่ใช่เอา type ไปยัดให้พอดีกับ API เดิมทีหลัง
ผมถึงขั้นคิดว่าถ้าไม่ระบุ interface ให้ชัด ก็ไม่มีทางรู้ได้ว่า interface นั้นเรียบง่ายหรือซับซ้อน ผมเห็นด้วยกับอุดมคติที่ว่า “โค้ดคือเอกสาร” แต่ถ้าไม่มีโค้ดที่ระบุ interface ไว้ interface นั้นก็ถือว่าขาดเอกสารโดยนิยาม
ถึงกับอยากไปนั่งดูเหตุการณ์แบบนั้นข้าง ๆ เลยด้วยซ้ำ ในสายงานของผม logic เฉพาะโดเมนแทบจะเข้าใจไม่ได้ใน codebase แบบ dynamic type ขณะที่โค้ด static type สอน business logic ให้กับนักพัฒนา
คำว่า “โค้ดคือเอกสาร” ก็ยิ่งทำให้งงมากกว่า จากประสบการณ์ของผม ต้องมี static type โค้ดถึงจะกลายเป็นเอกสารได้ ถ้าไม่มี ก็ไม่มีทางรู้ว่า object มี property อะไรบ้าง หรือทำไมถึงตรวจ property ที่คิดว่าไม่มีอยู่ มีคอมเมนต์ก็จริง แต่แทบไม่เคยเห็นใครทิ้งคอมเมนต์ที่มีความหมายไว้เลย
ประสบการณ์ของผมตรงกันข้าม pattern ที่ dynamic มาก ๆ ใส่ type ให้ดีได้ยาก และ type system ที่ดีจะส่งเสริม pattern ที่เรียบง่ายกว่า ทำให้ type ก็เรียบง่ายขึ้นด้วย
การกำจัดขีดแดงเป็นเรื่องสำคัญ ขีดแดงหมายความว่ามีปัญหา และมันง่ายกว่าการค้นพบ error ด้วยวิธีอื่นมาก ผมไม่เข้าใจว่าทำไมถึงอยากไปเจอ error นั้นทีหลัง
พอมีคำว่าไม่ชอบ autocomplete ด้วย ก็ทำให้ผมไปอยู่ฝั่งที่ไว้ใจคนต่อต้าน static type ไม่ได้ โปรแกรมเมอร์ที่ไม่อยากให้คอมพิวเตอร์ช่วยเขียนโปรแกรมนี่น่าสงสัยมาก
แต่สิ่งนั้นไม่ได้ลดทอนข้อดีทางเทคนิคของมัน เทคโนโลยีบางอย่างอาจยอดเยี่ยมพร้อมกับที่คนรอบ ๆ มันอาจเต็มไปด้วยความอวดรู้ก็ได้
ข้ออ้างที่ว่า dynamic type ทำให้เขียนโค้ดเรียบง่าย ฟังเหมือนพูดว่า “ขับรถโดยปิดตาจะดีเพราะทำให้ขับช้าลง” ถ้าเป้าหมายเป็นแบบนั้นก็ใช้ linter จำกัดความยาวบรรทัดหรือจำนวนพารามิเตอร์ได้ ไม่จำเป็นต้องสร้างข้อจำกัดแบบอ้อม ๆ
type ไม่ใช่คำตอบเดียว แต่ผมมองว่าเป็นเครื่องมือที่ควรหยิบมาใช้ก่อน เพราะ ผลตอบแทนต่อการลงทุน สูงมาก ลงทุนแทบไม่มี แต่ได้ประโยชน์มาก
ผมเห็นด้วยกับคำว่า “โค้ดคือเอกสาร” แต่ type ก็เป็นส่วนหนึ่งของโค้ด ดังนั้นผมอยากพูดว่า “โค้ดคือเอกสาร และ type ก็เป็นส่วนหนึ่งของโค้ด”
พื้นที่ที่เราอยู่คือวิศวกรรม ไม่มีคำตอบที่ถูกเพียงหนึ่งเดียว และทุกอย่างคือการประนีประนอม กลับกัน นั่นแหละคือเหตุผลที่งานของเราไม่ถูกทำให้เป็นอัตโนมัติแล้วหายไปในทันที
บรรยากาศในเธรดนี้ที่ “ดูถูก” กันเพราะความคิดเห็นหรือประสบการณ์ของวิศวกรคนอื่นนี่น่ารังเกียจจริง ๆ
ในสถานการณ์ที่ข้อมูลส่วนใหญ่เดินทางผ่านเครือข่ายเป็น JSON การต่อสู้เพื่อใช้ strong static typing มักเกิดขึ้นอย่างไม่สม่ำเสมอ
ควรใช้เครื่องมือทุกอย่างที่มี แต่ “ข้อมูล” ส่วนใหญ่นั้นเหลวกว่าที่คิดมาก เหตุผลที่ผู้คนเก็บเบอร์โทรศัพท์เป็น string ไม่ใช่เพราะขี้เกียจ แต่เพราะครั้งหนึ่งเคยเข้าใจผิดว่าสามารถทำให้เป็น type ที่แข็งแรงกว่าได้ แล้วเจ็บตัวมามากเกินไป ชื่อ ที่อยู่ และรหัสไปรษณีย์ก็เช่นกัน
ค่าเหล่านี้ต้องรับมาจากผู้ใช้ และในทางปฏิบัติก็แทบมีแต่วิธี parse ข้อความเท่านั้น ถ้าสร้างระบบที่ไม่เก็บข้อความต้นฉบับก่อน parse ไว้ สักวันหนึ่งแทบจะแน่นอนว่าจะต้องเสียใจ
ผมคิดว่าชั้นที่เก็บข้อความอินพุตเดิมไว้ พร้อมกับให้ข้อมูลแบบมี type เป็นชุดแก่ผู้ใช้ฝั่ง backend คือทางที่ดีที่สุด แต่ก็ต้องชั่งน้ำหนักว่าพื้นที่เล็ก ๆ ของแต่ละคนมีผลตอบแทนคุ้มการลงทุนระดับนั้นหรือไม่
ถ้าต้องทำการประเมินหนัก ๆ ก็น่าจะต้องมีชั้นที่แปลงเป็น SAT หรือโมเดลเชิงตัวเลขอื่น ๆ ในโลกนั้น ตัวเลขคือ abstraction ถ้าพยายามทำด้วยวิธีอื่น แทบจะต้องเจ็บปวดแน่นอน ควรมีชั้นที่แปลปัญหาเป็น formulation และแปลพื้นที่คำตอบเป็น domain และ type สามารถช่วยตรงนี้ได้ แต่ “type” ที่ได้รับความสนใจจริง ๆ บ่อยครั้งกลับไม่ใช่ type แบบนี้
ด้วยไลบรารีอย่าง Serde และ Pydantic เราใช้แนวทางที่ว่า deserialization คือ validation เราตรวจสอบข้อมูล JSON ทั้งหมดก่อนสร้างเป็น struct ในโค้ด
คล้ายกับตัวอย่าง Redis แม้จะรับ JSON ผ่านเครือข่าย แต่หลังจากตรวจสอบครบถ้วนแล้ว เมื่อมาถึงโค้ดก็มั่นใจได้ว่าเป็น type ที่มีรูปแบบถูกต้อง ดังนั้นในโค้ดจึงสามารถสมมติได้ว่า email type คืออีเมลที่ถูกต้อง และ ID type คือ ID ที่ถูกต้อง
การสลับฟิลด์ชื่อกับฟิลด์ที่อยู่แทบจะเป็น error เสมอ และ type system สามารถบังคับเรื่องนั้นได้
ถ้าการพิมพ์ผิดกลายเป็น runtime error นั่นไม่ใช่การ “เคลื่อนที่ได้เร็วขึ้น” และถ้าเวลาจะเปลี่ยน function signature แล้วต้อง grep ทั้ง codebase เพื่อหาจุดที่เรียกใช้ทั้งหมด และภาวนาให้แก้ครบ ก็ไม่ใช่การ “มีประสิทธิภาพมากขึ้น” เช่นกัน
type เป็นสิ่งที่ดี แต่ถ้าอะไรมากเกินไปก็เป็นปัญหาได้ ถ้าตั้งเป้าชีวิตว่าจะ encode business logic ทั้งหมดลงใน type system ก็จะเกิดความยุ่งเหยิงที่เข้าใจยากยิ่งกว่าการไม่มี type เลย ถ้าชื่อ type ในข้อความ error ยาวจนไม่พอดีในหนึ่งบรรทัด แปลว่าไปไกลเกินไปแล้ว
ถ้าจะให้ยุติธรรม มันมีไว้เพื่อให้เข้ากับโค้ด pure JS เก่า ๆ และตัวแปรที่น่าสงสารตัวนั้นก็อาจมีค่าได้สารพัดแบบ
ผมยังรู้สึกขอบคุณ TypeScript ตลอดไป แต่ถ้าในอนาคตมี paper เรื่อง “ยุคที่ type ไปไกลเกินไป” แล้วมีโค้ดแบบนั้นอยู่ในนั้น ผมก็คงไม่แปลกใจ
from pdb import set_trace: set_trace()เพื่อแก้ไขแบบ interactive ได้แต่พอโปรแกรมซับซ้อนขึ้นนิดเดียว สถานการณ์ก็เปลี่ยนไป พอส่งข้อมูลข้ามระบบผ่าน queue ใช้ async, thread, multiprocessing และใช้ compiled binary library ในส่วนหลักด้าน performance สุดท้ายก็จะรู้สึกว่าอยากให้เขียนทุกอย่างด้วย Erlang ไปเลย
มีแนวทางโดยรวมอย่าง test ที่เข้มงวดมากพร้อม code coverage 100% หรือเปล่า?
ถ้าตอน build หรือ compile จับจุดที่เรียกใช้ไม่ได้ นั่นก็ไม่ใช่การใช้ static type แล้ว
การพิมพ์ผิดสามารถทำให้เกิดโค้ดผิดได้แม้ในภาษาที่มี static type แข็งแกร่งที่สุด ไม่อย่างนั้นการเขียนโค้ดมันหมายความว่าอะไรกันแน่? ระหว่าง runtime error กับการได้ผลลัพธ์ผิดโดยไม่มี error อะไรแย่กว่ากัน?
การรันโปรเจกต์ Python อาจเร็วกว่า compile C++ และภาษา dynamic type ก็อาจมีวิธีค้นหา function call ที่ดีกว่า grep ได้
คำกล่าวที่ว่าถ้าไม่ใช้ type แล้วจะได้ข้อดีคือพัฒนาได้เร็วขึ้น ก็ไม่ตรงกับประสบการณ์ของผมเช่นกัน static type ทำให้การเขียนโปรแกรมประจำวันเร็วขึ้น
เรื่องที่ IDE ดีขึ้นเพราะ static type ได้พูดถึงไปทีหลัง แต่แม้ใน REPL ก็รู้สึกได้ type error ที่จับได้แบบ static ให้ข้อความ error ที่มีความหมายและใกล้กับต้นตอจริงมากกว่า และช่วยให้แก้ได้เร็วกว่ารuntime error
มันยังลดภาระที่ต้องคิดเรื่อง type อย่างระมัดระวังเกินไปด้วย เพราะคอมไพเลอร์รักษาวินัยให้ ผมจึงต้องใส่ใจน้อยลง สามารถเดินหน้าได้เร็วขึ้นด้วยความมั่นใจว่า error กลุ่มใหญ่จะถูกจับได้ทันที
จากประสบการณ์ของผม static type system ใช้ง่าย ทำให้พัฒนาเร็วขึ้น และเพิ่มความน่าเชื่อถือ ต้นทุนที่เจอมาจนถึงตอนนี้มีแค่สองอย่าง คืออาจเรียนรู้ยากกว่า และ implement ยากกว่า
junior developer ที่จะจ้างมาในอีก 6 เดือนข้างหน้าจะปรับตัวกับโค้ดที่ไม่มี type ได้ช้ากว่ามาก
ยอมรับได้ว่าสำหรับบางคน การเขียนครั้งแรกอาจ “เร็วกว่า” แต่หลังจากนั้น developer ทุกคนที่ต้องอ่านโค้ดนั้นจะช้าลง
การคิดให้ชัดว่าใส่อะไรเข้าไป เอาอะไรออกมา และเพราะอะไร อาจดีกว่าการทำอะไรปนเปที่แค่ผ่านคอมไพเลอร์
ผมมองว่าผู้เขียนผิดแทบทุกประเด็น ผมเองก็เคยคิดแบบนั้นมาหลายสิบปี แต่ในไม่กี่ปีมานี้เปลี่ยนความคิดไปโดยสิ้นเชิง
type ลด bug ได้ไหม? ไม่ อาจลดได้นิดหน่อยมาก ๆ แต่ไม่มีนัยสำคัญ ลองดูงานวิจัยที่เกี่ยวข้องได้
type ให้ developer experience ที่ดีกว่าไหม? ไม่ ใน REPL และ IDE ของผมมี definition และ variable ทั้งหมดอยู่แล้ว มี autocomplete สำหรับ symbol ทั้งหมด, call tree, การค้นหาจุดใช้งาน, refactoring อย่างมั่นใจ และการรัน/แทนที่/ห่อฟังก์ชันเดี่ยว ๆ ได้ทั้งใน REPL และในแอปพลิเคชัน
encode ทุกอย่างลงใน type system ได้ไหม? เป็นไปไม่ได้ ต้องมี runtime validation
ขอให้โชคดีกับการคลี่ type definition เมื่อ requirement เปลี่ยน นี่แหละคือจุดตัดสิน static type ทำให้ data model ของ domain ตามความเข้าใจ ณ ตอนนี้แข็งตัวเร็วเกินไป model นั้นจะเปลี่ยน และถ้าโชคร้าย ก็ต้องรองรับหลาย variation ของ domain model ภายใน runtime เดียวกัน โดยเฉพาะถ้าใช้ inheritance ก็ยิ่งลำบาก
จริงอยู่ที่ static type ให้แรงงัดมากกับ compiler optimization แต่ในบรรดาภาษา dynamic type ก็มีภาษาที่ให้ static type เป็น optional add-on เช่นกัน
ในหลาย use case โดยเฉพาะ enterprise development ภาษา functional ที่เน้น immutability ก่อนพร้อม dynamic type ให้ประโยชน์ระยะยาวอย่างมาก
category error ที่ผู้สนับสนุน type อย่างแข็งขันมักทำคือสมมติว่าจะเขียนโค้ดแบบเดียวกันแต่แค่ไม่มี type จริง ๆ แล้วไม่ได้เขียนแบบนั้น
ในแทบทุกประเด็น ข้อสรุปของผมตรงข้ามกันเลย แน่นอนว่าคำพูดที่ว่าต้องมี runtime validation นั้นถูก แต่ runtime validation ส่วนใหญ่หลีกเลี่ยงได้
เรื่องการเปลี่ยน requirement ผมกลับมองว่า static type ทำให้ปรับตัวง่ายขึ้น ใน dynamic type system ที่ผมเจอ สมมติฐานสำคัญเกี่ยวกับ data structure กระจัดกระจายอยู่ทั่วไป บางครั้งถูกตรวจแบบ dynamic เป็น pre/post-condition บางครั้งอยู่แค่ใน test หรือไม่ถูกตรวจเลย
การเปลี่ยน requirement จึงน่ากลัว เพราะต้องไล่ reasoning ผลกระทบต่อสมมติฐานโดยนัยเหล่านี้ทั้งหมด การรันแอปด้วยโค้ดใหม่อาจง่าย แต่การรู้ว่าไม่ได้ทำให้ code path หายากที่คิดไม่ถึงพังนั้นยากมาก
ผมชอบขั้นตอน static analysis ที่บอกว่า “คุณเปลี่ยน interface นี้ แล้วรู้ไหมว่า code path ตรงนี้พึ่งพาส่วนนั้นอยู่?” มากกว่า static type ไม่ใช่วิธีเดียว แต่ผมมองว่าภาระน้อยกว่าการมี dynamic validation และ test ในระดับเดียวกันมาก
type system ไม่ได้กำจัด runtime validation แต่ถ้าใช้ถูกต้องจะ ลดลงอย่างมหาศาล
ส่วนที่ดีที่สุดคือเมื่อ requirement เปลี่ยน คอมไพเลอร์จะบอกอย่างแม่นยำว่าต้องแก้อะไรเพื่อให้มันกลับมาทำงานอีกครั้ง ถ้าทำเรื่องเดียวกันในภาษา dynamic ก็ต้องไล่ตามเอง รอให้ unit test fail และภาวนาอย่าให้มี path ที่หลุดรอดไป
ผมหาตำแหน่งทั้งหมดที่ type หนึ่งถูกใช้ได้อย่างมั่นใจมากกว่า แล้วดูได้ว่าแต่ละจุดต้องเปลี่ยนหรือไม่ ในสภาพแวดล้อม dynamic type งานนี้ยิบย่อยและน่ารำคาญกว่ามาก
ในฐานะนักพัฒนาที่เคยเขียน C++, Python, JS มาหลายแสนบรรทัด ผมก็ยังไม่ค่อยแน่ใจนัก มันไม่ได้ชัดเจนขนาดนั้น
ทั้งสามภาษาต่างก็ทำงานได้มีประสิทธิภาพ แต่โดยทั่วไป Python ชนะ อย่างไรก็ตาม ผมคงไม่เขียนเกมเอนจินหรือวิดีโอโคเดกด้วย Python
JavaScript ไม่สม่ำเสมอและแปลก แต่ legacy ของ Netscape ก็ผูกพวกเราทุกคนไว้กับมันไปแล้ว
ในสไตล์ที่เน้นเชิงวัตถุมาก ๆ และมีคลาสซ้อนกันขนาดใหญ่จำนวนมาก static type ตอนคอมไพล์/พาร์สสามารถช่วยลดความผิดพลาดได้มาก แต่ผมเริ่มมองว่า object-oriented โดยรวมแทบจะเป็นหายนะ และฟังก์ชันเรียบง่ายกับข้อมูลที่มีโครงสร้างแทบจะชนะเสมอในด้านความเรียบง่ายและการดูแลรักษา
language server และ IDE สมัยใหม่สามารถจับข้อผิดพลาดจากการพิมพ์ได้มากแม้ระหว่างพัฒนา JS/Python ผมค่อนข้างเข้มงวดกับหลายส่วนของการเขียนโปรแกรม แต่ไม่ได้มีจุดยืนแรง ๆ เรื่อง static type เทียบกับ dynamic type ทั้งสองแบบต่างก็มีโปรเจกต์ที่ประสบความสำเร็จนับล้าน
ผมมองว่าการเพิ่ม productivity ที่ใหญ่ที่สุดที่ภาษาหนึ่งจะให้ได้คือ garbage collection เลยอยากรู้มากกว่าว่า Go ซึ่งมีไวยากรณ์และ type ที่เรียบง่ายกว่ามากจะเทียบกันอย่างไร Java ก็อาจดีกว่า แม้จะ verbose แต่สำหรับงานส่วนใหญ่ cognitive load เป็นเพียงเศษเสี้ยวของ C++
ผมพิมพ์ผิดบ่อย และมักใส่ลำดับอาร์กิวเมนต์ผิดด้วย โดยเฉพาะตอนทำ machine learning type ช่วยได้มาก สิ่งที่อยากเลี่ยงที่สุดคือใช้เวลา 30 นาทีกับการประมวลผลข้อมูล แล้วโค้ดเทรนโมเดลมาตายทีหลัง
ผมมองว่า gradual typing ของ Python เป็นจุดกึ่งกลางที่ดีมากระหว่างการทำ rapid prototyping กับแนวทางที่ค่อยใส่ type annotation เมื่อฟังก์ชันสุกงอมพอแล้ว
การถกเถียงว่า type ที่เข้มงวดดีกว่า type ที่อ่อนนั้นถือว่าจบไปแล้ว แต่เรื่องว่า static type ดีกว่า dynamic type หรือไม่นั้นยังไม่จบ
ฝ่ายสนับสนุน static type คิดว่า compiler ควรตรวจสอบ type invariant เพื่อยืนยัน “ความถูกต้อง” ส่วนฝ่ายสนับสนุน dynamic type มองว่านั่นเป็นการเสียเวลา
ผมอยู่ฝ่ายหลังอย่างชัดเจน เพราะ compiler ตรวจได้แค่ความถูกต้องของ type ไม่ใช่ความถูกต้องของโปรแกรม ความถูกต้องของ type เป็นสิ่งจำเป็นต่อความถูกต้องของโปรแกรม แต่ไม่เพียงพอ ฝ่ายสนับสนุน static type มักยอมรับจุดนี้ไม่ได้ และเข้าใจผิดว่า static type รับประกันอะไรได้มากกว่าความเป็นจริง
ลองดูตัวอย่าง
birthdayGreetingในบทความ ผู้เขียนดีใจที่ static type จับบั๊กในbirthdayGreeting("John", "20")ได้ เพราะ"20"ไม่ใช่ตัวเลข แต่birthdayGreeting(" ", 123)กลับจับไม่ได้" "ไม่ใช่ชื่อ และbirthdayGreeting("Anna," -12335)ก็จับไม่ได้เช่นกัน ในทางกลับกันbirthdayGreeting("Anna" 4.5)ถูกจับได้ ทั้งที่ 4.5 ก็ถือเป็นอายุได้ จึงอาจเรียกได้ว่ามันผิดเสียเองเรื่องนี้สำคัญ “บั๊กด้าน type” นั้นจับได้ง่ายจนน่าขัน แต่ บั๊กเชิงความหมาย อาจซ่อนอยู่ได้นานหลายปี เช่น overflow ของยอดบัญชีที่เก็บเป็น
uint, ตัวเลขที่ควรเป็นจำนวนเฉพาะในตำแหน่งหนึ่งแต่ไม่ใช่, list ที่ไม่ควรว่าง เป็นต้น แม้แต่ dependent type ก็ยังรับประกัน invariant แบบนี้ไม่ได้ถ้าไม่เชื่อก็ลองค้นหาบั๊กร้ายแรงที่ทำให้ยานอวกาศระเบิดหรือเกิดอุบัติเหตุรถยนต์ดู เท่าที่ผมรู้ แทบไม่มีกรณีไหนที่เกิดจาก type error จริง ๆ และส่วนใหญ่ท่วมท้นเป็นข้อผิดพลาดเชิงความหมาย
[1] คนส่วนใหญ่ไม่เข้าใจว่าควรมอง type อย่างน้อยสองแกน คือ strong/weak และ static/dynamic แล้วก็ยังสับสนระหว่าง weak typing กับ dynamic typing อยู่เรื่อย ๆ C เป็น static และ weak typing, Python เป็น strong และ dynamic typing, ส่วน JavaScript เป็น weak และ dynamic typing
นั่นแหละคือเหตุผลที่ผมอยู่ฝั่ง static type เพราะมันง่ายมากจนจัดการได้แบบ declarative วางไว้ข้าง ๆ โค้ดที่ต้องตรวจ พร้อม feedback ทันที และครอบคลุมทุก call site รวมถึงทุก sub-expression และ statement ได้
type annotation ไม่ได้หมายความว่า semantics หรือ domain logic ถูกต้อง เรื่องนั้นยังต้องทดสอบอยู่ แต่สามารถแทนที่ test ยิบย่อยหลายสิบตัวที่ตั้งฉากกับ logic ที่เราสนใจได้ พูดตรง ๆ แทบไม่มีใครเขียน test แบบนั้นได้ครบถ้วนหรอก
ตอนท้ายบทความผมบอกไว้ว่าเราจะ validate ตอนสร้าง type จากที่อย่าง user input ดังนั้น type
Nameจึง valid เสมอ และ" "ไม่ใช่ชื่อ เพราะ type รับประกันว่าเป็นชื่อที่ valid ใน codebase ของเราเลยจับได้แน่นอนbirthdayGreeting("Anna" 4.5)กับbirthdayGreeting("Anna," -12335)ใน JS นั้นnumberเป็น floating point จึงถือว่า valid ในทางปฏิบัติ เพียงแต่ตอนเขียนผมนึกถึงจำนวนเต็มอยู่ นี่เป็นอีกกรณีที่ type ที่เข้มงวดกว่า TS เช่น Rust ช่วยนิยาม invariant ได้ดีกว่าสรุปคือผมพยายามยกตัวอย่างง่าย ๆ จึงไม่ได้กำหนด type ทุกอย่างอย่างเข้มงวดเหมือนปกติ และผลก็คือบั๊กที่ type ควรจับได้กลับโผล่ออกมาให้เห็นมากขึ้น
Nameที่แทนชื่อที่ valid เสมอ และ typeAgeที่แทนอายุที่ valid เสมอเอาการ validate ไปไว้ที่ constructor ของ type เหล่านั้นที่เดียว แล้ว method ต่าง ๆ อย่าง
birthdayGreetingก็ใช้ค่าของ type นั้นได้โดยไม่ต้องรับผิดชอบเพิ่มผมไม่รู้ว่าจะ implement pattern นี้ให้ดีได้อย่างไรถ้าไม่มี type checking หรืออย่างน้อย optional type hint กับ static analysis ส่วนการ validate input ในทุก method นั้นเป็นภาระเกินไป และการสมมติว่า caller จะส่งค่าที่ valid มาให้แล้วค่อยเขียน test เพื่อไม่ให้เกิดเรื่องใหญ่ ก็ไม่น่าพอใจเช่นกัน
birthdayGreetingรับช่วง 1~150 นั้นทำได้ง่ายใน ADAในบรรดาปัญหาสาธารณะเกี่ยวกับยานอวกาศ ก็มีบางกรณีที่น่าจะจับได้ด้วย type checking ที่ดีกว่า การแปลงระบบเมตริก/ระบบอิมพีเรียลก็ใส่หน่วยลงไปใน type ได้ เพียงแต่กรณี [2] น่าจะเป็นความผิดพลาดฝั่ง integration test มากกว่า
แน่นอนว่า type checking ไม่อาจค้นหาปัญหาทั้งหมดของโค้ดได้ โดยเฉพาะปัญหา algorithm และไม่ได้ทดแทน testing แต่ feedback ทันทีและ type hint ในช่วงพัฒนานั้นมีค่ามาก
ในตัวอย่าง อาจเปลี่ยนสตริงชื่อให้เป็น type หรือ object ของบุคคลก็ได้
[1]: https://en.m.wikipedia.org/wiki/Ariane_flight_V88
[2]: https://en.m.wikipedia.org/wiki/Mars_Climate_Orbiter
ไม่มีวิธีแก้ที่สมบูรณ์แบบ แต่มีวิธีแก้ที่มีคุณค่าอยู่มากมาย
มีการบรรจบกันค่อนข้างมากในประเด็นนี้ ตอนนี้ภาษาส่วนใหญ่มี type inference ในระดับ statement อยู่บ้างแล้ว แม้แต่ C++ ก็มี
autoสิ่งนี้ช่วยลด type boilerplate ในโค้ดลงได้มาก ยุคที่ต้องเขียนชนิด iterator ยาว ๆ ทั้งหมดในลูป
forของ C++ นั้นผ่านไปแล้วการประกาศฟังก์ชันและฟิลด์ของ struct คือจุดที่ต้องมีข้อมูลชนิดเพื่ออ่านโค้ดได้ เมื่อโปรแกรมยาวเกินไม่กี่ร้อยบรรทัด หรือมีนักพัฒนามากกว่าหนึ่งคน annotation บางส่วนก็เป็นสิ่งจำเป็น
แน่นอนว่าการคัดค้านหลักมาจากผู้ใช้ Python และ JavaScript Python แปะระบบชนิดแบบคำแนะนำที่ค่อนข้างประหลาดเข้ามาทีหลัง ส่วน JavaScript ก็แปะ TypeScript เข้ามาทีหลัง ทั้งสองอย่างเป็นระบบชนิดที่เสริมเข้ามา และถูกใช้ในสภาพแวดล้อมที่โค้ดมีชนิดกับไม่มีชนิดปะปนกัน นั่นเจ็บปวด
LISP ก็เคยแปะระบบชนิดเข้ามาทีหลังเมื่อหลายสิบปีก่อนด้วย “flavors” และ Common LISP Object System และมันก็ไม่ได้ดูดีนัก บทเรียนคือถ้าแปะระบบชนิดเข้ามาทีหลัง มันจะเละเทะ
มันมีฟีเจอร์ดี ๆ เช่น
Optionalที่บังคับให้ตรวจสอบก่อนใช้Noneหรือ structural subtyping ผ่านtyping.Protocolถ้า Python ถูกออกแบบโดยคำนึงถึงชนิดตั้งแต่แรกก็คงดีกว่านี้ แต่เมื่อคิดถึงข้อกำหนดว่าต้องผสานกับโค้ด Python เดิมและห้ามทำให้โค้ดใด ๆ พัง ก็ถือว่าทำได้ค่อนข้างดีปัญหาที่ใหญ่กว่าของ static typing ใน Python คือ ecosystem และขนบปฏิบัติ โดยเฉพาะยิ่งแย่ลงเมื่อมีนักพัฒนาจำนวนมากใช้ Python ในฐานะ data scientist โดยพฤตินัย พวกเขาขี้เกียจเขียน method signature ที่เหมาะสม จึงใช้
*args/**kwargsเกินควรเป็นเรื่องธรรมดามากที่เมธอดต่าง ๆ ส่ง DataFrame หรือ dictionary กันไปมาเหมือนถุงใส่ของจิปาถะ ได้คะแนนพิเศษถ้าเมธอดเพิ่มหรือลบคอลัมน์หรือฟิลด์ จนคุณไม่รู้ว่ามีอะไรอยู่ในถุงข้อมูลนั้นจนกว่าจะรันโค้ดหรืออ่านทุกบรรทัด
แน่นอนว่าแทบทุกภาษาก็ทำแบบคล้ายกันได้ เช่น ใน C# จะใช้ทุกชนิดเป็น
dynamicหรือให้เมธอดของ Go รับinterface{}ทั้งหมดก็ได้ แต่ Python ส่งเสริมแนวทางแบบนี้อย่างแข็งขันมานาน และแม้ตอนนี้ tutorial สำหรับมือใหม่จำนวนมากก็ยังนำเสนอว่า “ถ้ารับ*kwargsก็ไม่ต้องเปลี่ยน function signature” ราวกับเป็นฟีเจอร์ขั้นสูงสำหรับคนฉลาด ไม่ใช่กับดักอันเลวร้ายมันจำเป็นสำหรับการ migrate แบบค่อยเป็นค่อยไป และเข้าใจได้อย่างสมบูรณ์ว่าทำไมจึงทำงานในลักษณะนี้
Flavors ถูกนำเข้าไปใน Lisp ที่ไม่มีระบบชนิด และต่อมา CLOS ถูกเพิ่มเข้าไปใน Common Lisp ซึ่งเป็น Lisp ที่มีระบบชนิดอยู่แล้ว
ผู้คนมักคลั่งไคล้สิ่งที่ยึดถือด้วยอารมณ์มากกว่าเหตุผลเสมอมา
ข้ออ้างว่า “ชนิดช่วยลดบั๊ก” เป็นข้ออ้างที่ฟังดูน่าเชื่อ มากกว่าจะเป็นความจริง
https://blog.metaobject.com/2014/06/the-safyness-of-static-t...
ไม่ใช่ว่าไม่มีคนพยายามพิสูจน์ อาจพูดได้ด้วยซ้ำว่าข้ออ้างนั้นถูกหักล้างแล้ว
อย่างไรก็ตาม ส่วนตัวผมชอบ static typing โดยหลัก ๆ เพราะ ผลด้านเอกสารประกอบ และอาจไม่ใช่เรื่องบังเอิญที่ผลเชิงบวกซึ่งมีหลักฐานเชิงประจักษ์หนักแน่นจริง ๆ ก็มีอยู่แค่ด้านนั้น
การป้องกัน regression อาจสำคัญกว่าการเขียนโค้ดให้ถูกตั้งแต่แรก และเมื่อดูโค้ดที่ไม่วิวัฒน์ต่อ ก็จะประเมินส่วนนี้ไม่ได้
อย่างเป็นรูปธรรม การลบฟิลด์ของอ็อบเจ็กต์ในโปรเจกต์ JavaScript ล้วนขนาดใหญ่โดยเนื้อแท้แล้วเป็นเหมือนเดินในทุ่งทุ่นระเบิด และเคยก่อบั๊กมาแล้วมากมาย ในทางกลับกัน ในโปรเจกต์ TypeScript ที่ครบถ้วน สามารถทำการเปลี่ยนแปลงเดียวกันได้อย่างมั่นใจ
ผมคิดว่าสิ่งนี้ถือเป็นหลักฐานเชิงประจักษ์ได้ว่า static typing ที่แข็งแรงมากช่วยลดบั๊ก
ท้ายที่สุด อย่างที่คอมเมนต์จำนวนมากบอก ชนิดไม่ใช่เรื่องสองขั้วใช่/ไม่ใช่ แต่เป็นสเปกตรัมใหญ่ในหลายแกน เช่น static/dynamic, strong/weak ระบบชนิดแต่ละแบบก็แตกต่างกันมาก และวิธีที่ผู้คนนำระบบชนิดนั้นไปใช้กับปัญหาก็แตกต่างกันมากด้วย
แม้ในภาษาที่มี static typing และ strong typing ก็ยังสามารถแทนทุกอย่างด้วยสตริงแล้วแปลงไปมาอยู่เรื่อย ๆ ได้ ซึ่งโดยสาระแล้วคือการทำงานเหมือนภาษา dynamic typing ในทางกลับกัน หากใช้เครื่องมือที่ระบบชนิดมอบให้ สร้างคลาสที่แทนค่าที่ถูกต้องตามกฎ และยืนยัน invariant สำคัญ ๆ ก็จะได้ประโยชน์
ผลิตภาพเพิ่มขึ้นไม่ใช่แค่หลายเท่า แต่เพิ่มเป็นหลักลำดับขนาด ผมพูดจากประสบการณ์ที่ใช้ภาษาหลายภาษาอย่างจริงจังซึ่งครอบคลุมพื้นที่กว้างของสเปกตรัมนี้ เช่น C, C++, Java, Python, JavaScript, TCL
โค้ดที่ไม่ได้แตะมาสักพัก ไม่ว่าจะเป็นโปรเจกต์ปัจจุบันหรือ dependency จะให้เหตุผลเกี่ยวกับมันได้ง่ายขึ้นมาก ไม่ต้องหลุดไปตามทางแยกอยู่เรื่อย ๆ เพื่อหาว่าจริง ๆ แล้วอ็อบเจ็กต์ที่ฟังก์ชันหนึ่งคืนมานั้นทำอะไรได้บ้าง จึงโฟกัสกับปัญหาตรงหน้าได้มากขึ้น
ความโล่งใจดี ๆ ตอน compile ผ่านก็มีอยู่ แต่เป็นเรื่องรอง
ความยึดติดทางอารมณ์มีเพียงความโกรธเมื่อต้องเจอกับคำว่า “แต่มันก็ไม่ได้กำจัดบั๊กทั้งหมดนี่!”
การถกเถียงนี้ให้ความรู้สึกแบบนั้นพอดี
ผมแค่อยากได้ความมีเหตุผลง่าย ๆ ที่ compiler จะบอกว่า “ไม่ได้” เมื่อพยายามใช้ hashmap เหมือนเป็น Apple หรือ String