ทิ้ง Haskell ไว้เบื้องหลัง
(journal.infinitenegativeutility.com)- โปรแกรมเมอร์ที่ใช้ Haskell เป็นภาษาหลักตั้งแต่ราวปี 2009 ถึงราวปี 2019 ปัจจุบันไม่ได้วาง Haskell เป็น ตัวเลือกเริ่มต้น สำหรับโปรเจกต์ส่วนตัวและการหางานอีกต่อไป
- เสน่ห์ของ Haskell ไม่ได้อยู่ที่ monad หรือ DSL เองเท่านั้น แต่อยู่ที่ การให้เหตุผลกับโค้ดเชิงพีชคณิต รวมถึงการรีแฟกเตอร์อย่างปลอดภัยและการแสดง invariant ด้วยระบบ type ที่แข็งแกร่ง
- เหตุผลที่ถอยห่างออกมาคือการสะสมของวัฒนธรรมที่ชอบฟีเจอร์ type ขั้นสูง เครื่องมือที่ไม่ลื่นไหล และ การเปลี่ยนแปลงที่ไม่เข้ากันย้อนหลังอย่างต่อเนื่อง ใน GHC และไลบรารีมาตรฐาน
- ถึงอย่างนั้น higher-order functions, สัญกรณ์
do, lenses, ไลบรารีเชิง declarative และ abstraction ของ type ขั้นสูงยังคงเป็นข้อดีที่ภาษาอื่นทดแทนได้ยาก - Haskell มีคุณค่าสูงในฐานะภาษาที่ควรเรียนเพื่อเป็นโปรแกรมเมอร์ที่ดีขึ้น แต่หากจะใช้เป็นภาษาหลักขององค์กร ต้องยอมรับต้นทุนด้านคุณภาพเครื่องมือและเสถียรภาพของภาษา
ช่วงเวลาที่ใช้ Haskell เป็นภาษาหลักและสถานะในปัจจุบัน
- เป็นเวลาประมาณ 10 ปีที่ Haskell เป็น ภาษาเริ่มต้น สำหรับโปรเจกต์ใหม่
- หลังจากได้รู้จัก Haskell ราวปี 2009 ผู้เขียนมองตัวเองว่าเป็นโปรแกรมเมอร์ Haskell จนกระทั่งย้ายไปทำงานที่ใช้ Ruby และ C++ เป็นหลักราวปี 2019
- ใช้ Haskell ในหลายงาน เช่น เครื่องมือบรรทัดคำสั่ง, เว็บเซอร์วิส, แอปพลิเคชันกราฟิก และสคริปต์ขนาดเล็ก
- ตอนนี้ไม่ได้ปฏิเสธ Haskell โดยสิ้นเชิง แต่ก็ไม่ใช่ภาษาเริ่มต้นแม้ในโปรเจกต์ส่วนตัว และไม่ได้ตั้งใจค้นหา “งาน Haskell” เหมือนในอดีต
- การตัดสินนี้ไม่ใช่ข้อสรุปว่า Haskell เป็นภาษาที่แย่ แต่ใกล้เคียงกับการอธิบายเหตุผลส่วนตัวที่ถอยห่างจาก Haskell มากกว่า
เหตุผลที่เคยหลงใหล Haskell
-
คุณสมบัติที่ทำให้จัดการโค้ดแบบพีชคณิตได้
- เสน่ห์ที่ใหญ่ที่สุดของ Haskell คือสามารถ ให้เหตุผลกับโค้ดในเชิงสัญลักษณ์และพีชคณิต ได้
- ตัวอย่างเช่น เมื่อมีฟังก์ชัน
double n = n + nการแปลงdouble xเป็นx + xจะถูกต้องเสมอใน Haskell - ในภาษา imperative จำนวนมาก
double(i++)กับ(i++) + (i++)อาจทำงานต่างกันได้- แบบแรกเพิ่มค่า
iหนึ่งครั้ง - แบบหลังเพิ่มค่า
iสองครั้ง
- แบบแรกเพิ่มค่า
- คุณสมบัตินี้ทำให้สามารถนำการแปลงเชิงกลเล็ก ๆ มาต่อกันเพื่อทำ การรีแฟกเตอร์ครั้งใหญ่ ได้อย่างปลอดภัยขึ้น
- การรีแฟกเตอร์ครั้งใหญ่ในโค้ดเบส Ruby อาจทำให้กังวลมาก แต่ในโค้ดเบส Haskell อาจทำได้ง่ายกว่ามาก
-
ระบบ type ที่เข้มงวดแต่ไม่รกเกินไป
- ระบบ type ของ Haskell เข้มงวด ขณะเดียวกันด้วย type inference จึงไม่มีการเขียน type annotation มากเกินไปเหมือน Java
- typeclass ช่วยลดความยุ่งยากแม้ในสถานการณ์ที่ต้องใส่ใจกับ type
- โค้ด Haskell ที่ดีมีคุณสมบัติแบบ “เปราะอย่างเหมาะสม (brittle)”
- abstraction ที่ผิดพลาดจะไม่โค้งงอเงียบ ๆ จนยอมให้โปรแกรมที่ผิดทำงานได้
- หากมีปัญหา การคอมไพล์จะล้มเหลว
- type
NonEmptyแทนลิสต์ที่มีองค์ประกอบอย่างน้อยหนึ่งตัว- operation ที่รักษาความยาวอย่าง
mapจะคืนค่าNonEmpty - operation ที่เพิ่มความยาวอย่าง
appendก็คืนค่าNonEmpty - operation ที่ผลลัพธ์อาจว่างได้อย่าง
filterจะคืนค่าลิสต์ทั่วไป
- operation ที่รักษาความยาวอย่าง
- ฟังก์ชันที่ต้องการรายการ path ของไฟล์เป็น
NonEmptyไม่สามารถส่งอาร์กิวเมนต์จาก command line เข้าไปตรง ๆ ได้ ต้องตรวจสอบก่อนว่าไม่ว่าง - หากภายหลังเพิ่มขั้นตอนกรองนามสกุล ผลลัพธ์อาจว่างได้ จึงต้องตรวจสอบอีกครั้ง และคอมไพเลอร์จะกันเส้นทางที่เผลอส่งลิสต์ว่างเข้าไป
-
วิธีรักษา invariant ด้วย type
- เมื่อใช้ Haskell มานาน จะคุ้นเคยกับวิธีแสดง invariant ของโปรแกรมด้วย type แล้วให้คอมไพเลอร์ตรวจสอบ
- สิ่งนี้ไม่ได้หมายถึงการคำนวณระดับ type ที่ซับซ้อนเท่านั้น
- สามารถแยกแยะได้ว่ามีการ escape SQL แล้วหรือยังด้วย wrapper type อย่าง
RawStringและSanitizedString- แม้เว็บ API จะให้
RawStringมา ก็ทำให้ชั้นฐานข้อมูลรับได้เฉพาะSanitizedStringเท่านั้น
- แม้เว็บ API จะให้
- สามารถแปลง representation ชั้นนอกที่มีฟิลด์
Maybeจำนวนมาก ให้เป็น representation ภายในที่รับประกันว่าข้อมูลที่จำเป็นมีอยู่จริง - ยังสามารถทำให้ generic เท่าที่จำเป็นพอให้ทดสอบแยกอิสระได้
-
ภาษา DSL และ higher-order functions ที่เป็นธรรมชาติ
- Haskell ทำให้สร้าง ภาษาเฉพาะโดเมน ที่กระชับได้ง่ายแม้ไม่มี macro
- config-ini เป็นตัวอย่างไลบรารีสำหรับจัดการไฟล์ตั้งค่า INI
- ใช้อินเทอร์เฟซแบบ declarative แทนอินเทอร์เฟซ parser เชิง imperative
- map ส่วนหนึ่งของ type การตั้งค่าเข้ากับส่วนเฉพาะในโครงสร้างไฟล์ INI
- ใช้อินเทอร์เฟซเดียวกันเพื่ออ่าน เขียน และทำ diff-minimal update
- Ruby DSL อาจพึ่งพา dynamic metaprogramming และ monkeypatching ระดับ global แต่ Haskell DSL จำกัดให้อยู่เฉพาะไฟล์หรือบริเวณโค้ดบางส่วนได้ง่ายด้วยฟีเจอร์อย่างไวยากรณ์ที่ยืดหยุ่นและ monad overloading
- Haskell ยังทำให้งานกับ higher-order functions เป็นธรรมชาติ
(+)ใช้ได้ทันทีในฐานะฟังก์ชันที่รับอาร์กิวเมนต์ตัวเลขสองตัวแล้วบวกกัน- สามารถแสดงการคูณสมาชิกทีละตำแหน่งของลิสต์สองชุดได้สั้นมาก เช่น
pairwiseProduct = zipWith (*)
- อุปมาที่เหมาะคือ Haskell ทำให้จัดการฟังก์ชันได้ในแบบเดียวกับที่ Perl ทำให้จัดการสตริงได้
ปัจจัยสามอย่างที่ทำให้ถอยห่างจาก Haskell
-
บรรยากาศที่ชอบสไตล์เข้าใจยาก
- ปัจจัยแรกคือ ความนิยมเทคโนโลยีใหม่ในเชิงสไตล์
- ชุมชน Haskell ทดลองและสร้าง abstraction ใหม่ ๆ อย่างต่อเนื่อง
- แนวทางที่นำมาจาก abstract algebra หรือ category theory
- แนวทางที่ยกการคำนวณจำนวนมากขึ้นไประดับ type เพื่อจำกัดสถานะที่ผิดพลาด
- เว็บ API ระดับ type ของ Servant หรือ กรณีที่แสดงระบบเมนูด้วย comonad เป็นการทดลองเพื่อแสดงโดเมนปัญหาด้วยวิธีใหม่
- แต่ในโค้ดประจำวัน แนวทางทดลองเหล่านี้อาจสร้างปัญหามากกว่าข้อดีที่เห็นในตอนแรก
- ตัวอย่างหนึ่งในโปรเจกต์คอมไพเลอร์คือ node ของ AST มี type ของ expression เป็น type parameter
- หากสร้าง syntax tree ที่มี type error ตัวคอมไพเลอร์เองจะคอมไพล์ไม่ผ่าน
- ตอนแรกช่วยจับ bug ได้บางอย่าง
- เมื่อ optimization pass จำนวนมากไม่สอดคล้องกับวินัยของ type โค้ดก็ซับซ้อนขึ้นด้วยการ cast และการเอาใจ GHC
- สุดท้ายจึงลบ type parameter ออกจาก AST
- ในโปรเจกต์ที่ลองแนวทาง “fancy types” ประมาณ สามในสี่ ถูกประเมินว่ามีคุณค่าไม่เพียงพอ
- กระแส Simple Haskell ตั้งอยู่บนแนวคิดว่า จะได้ประโยชน์จากภาษามากที่สุดเมื่อหลีกเลี่ยงฟีเจอร์ทดลองและใช้ extension ให้น้อยที่สุด
- ฟีเจอร์ type ที่ซับซ้อนกว่านั้นแพร่หลายในชุมชนและไลบรารี
- abstraction ของ type ที่ซับซ้อนอาจทำให้การเปลี่ยนแปลงและบำรุงรักษายากขึ้น และเคยมีประสบการณ์ที่ความพยายามเปลี่ยน abstraction ที่ทำให้เรียบง่ายแล้วกลับไปเป็น abstraction ระดับ type สมัยใหม่ ทำให้ขนาดโค้ดเพิ่ม ประสิทธิภาพลด และอ่านยากขึ้น
- อย่างไรก็ตาม โครงสร้างข้อมูลที่ถูก index ตามแต่ละเฟสของคอมไพเลอร์อย่าง Trees that Grow ยังเป็นกรณี “fancy type-level feature” ที่มีประโยชน์
-
เครื่องมือที่ไม่โดดเด่นในงานประจำวัน
- ปัจจัยที่สองคือ ระบบเครื่องมือที่ไม่ค่อยลงตัว
- Haskell มี build system และเครื่องมือพัฒนาหลายตัวที่ใกล้เคียงมาตรฐาน
- โดยรวมแล้วเครื่องมือเหล่านี้ทำงานที่จำเป็นได้ แต่ยังไม่ถึงระดับที่ “น่าหลงรัก”
- มีเครื่องมืออย่าง
ormoluที่ทำสิ่งที่ควรทำโดยมีความยุ่งยากน้อยที่สุด แต่บางเครื่องมือมี bug ต่อเนื่องและต้องใช้ workaround - เครื่องมือ Haskell ดีขึ้นในช่วง 10 ปี แต่ ณ ปี 2019 ยังไม่มีเครื่องมือใดที่ใกล้เคียงกับความง่ายในการใช้และเสถียรภาพของ Rust Cargo
- แนวโน้ม Not-Invented-Here ที่พยายามสร้างสิ่งใหม่และดีกว่าอยู่เรื่อย ๆ ก็เป็นปัญหาเช่นกัน
- มีแนวโน้มถึงขั้นจัดการ format ข้อความของแพ็กเกจใหม่ด้วย hpack
- เป็นเรื่องน่าหงุดหงิดเมื่อความพยายามเหล่านี้ค้างอยู่ที่เสร็จ 95% เป็นเวลาหลายปี
- ไม่ได้หมายความว่าเครื่องมือ Haskell แย่มาก แต่ระดับ “พอใช้ได้” ไม่ตอบโจทย์มาตรฐานที่สูงขึ้นแล้ว
-
การเปลี่ยนแปลงที่ไม่เข้ากันย้อนหลังอย่างต่อเนื่อง
- ปัจจัยที่สามคือ การเปลี่ยนแปลงอย่างต่อเนื่อง ที่ภาษา Haskell และ GHC เผชิญ
- GHC ถูกมองว่าเป็น implementation สาธารณะของ Haskell ที่สมเหตุสมผลแทบจะเพียงหนึ่งเดียวในทางปฏิบัติ
- Haskell พยายามแก้ไขข้อผิดพลาดและความไม่สอดคล้องในอดีตอย่างค่อยเป็นค่อยไป แม้แต่ในไลบรารีมาตรฐานและส่วนพื้นฐานของภาษา
- การเปลี่ยนแปลงเหล่านี้อาจนำภาษาไปสู่ส่วนประกอบพื้นฐานที่สะอาดและสอดคล้องมากขึ้น แต่สำหรับผู้ใช้แล้วมันสร้างแรงเสียดทานเล็ก ๆ อย่างต่อเนื่อง
- Foldable-Traversable Prelude changes ในปี 2015 เป็นตัวอย่างของการเปลี่ยนแปลงลักษณะนี้
- โปรเจกต์ Haskell เก่าอาจ build ไม่ผ่านบน GHC รุ่นล่าสุด แม้ไม่มี dependency ภายนอก
- การเพิ่ม superclass
- การลบ import
- การเพิ่ม type signature เพราะ polymorphism กลายเป็นกำกวม
- การแก้มักเป็นเรื่องเล็ก แต่สะสมต่อเนื่อง และส่วนที่ค่อย ๆ ต่างจากเดิมก็เรียกร้องความสนใจจากผู้ใช้
- วิธีอัปเดตแบบ epochs ของ Rust ถูกนำมาเปรียบเทียบ เพราะระบุสัญญาความเข้ากันย้อนหลังสำหรับฟีเจอร์ stable และซ่อน breaking change ที่ตั้งใจไว้หลัง epoch
ข้อดีของ Haskell ที่ยังคงคิดถึง
- การเขียนโค้ดและรีแฟกเตอร์แบบพีชคณิตของ Haskell ยังคงเป็นจุดแข็ง
- Rust ก็มีข้อดีหลายอย่างร่วมกับ Haskell แต่ความมั่นใจว่าการรีแฟกเตอร์เชิงกลเล็ก ๆ จะรักษาความหมายของโปรแกรมไว้ได้นั้นไม่ง่ายเท่า Haskell
- ระบบ type ก็ให้ฟีเจอร์ที่ภาษาอื่นโดยทั่วไปไม่มี
- ภาษากระแสหลักในปี 2023 มีฟีเจอร์ static type พัฒนาขึ้นมากเมื่อเทียบกับปี 2009
- ถึงอย่างนั้น ฟีเจอร์อย่าง abstraction บน higher-kinded types ก็ยังเป็นจุดแตกต่างของ Haskell
- ระบบนิเวศไลบรารีของ Haskell มีทั้งดีและไม่ดีปนกัน
- ไลบรารีโอเพนซอร์สที่ดูแลอยู่ครึ่ง ๆ กลาง ๆ
- อาณาจักรเล็ก ๆ ที่ผู้เขียนแต่ละคนพยายามสร้างโลกใหม่ตามแนวทางของตัวเอง
- type คล้ายสตริงหลายแบบที่จำเป็นแต่ต่างกัน
- ถึงอย่างนั้น Haskell ก็มีไลบรารีและ abstraction ยอดเยี่ยมที่ภาษาอื่นทำได้เพียงครึ่งเดียว
- lenses เป็น abstraction ที่ทรงพลังซึ่งข้อดีจะยังไม่ค่อยปรากฏถ้าไม่ได้ลองใช้มากพอ
- Brick ให้ไลบรารี terminal UI ที่ดีมากด้วย block แบบ declarative
- diagrams และ music-suite ก็เป็นตัวอย่างที่ดีของ API แบบ declarative
- สัญกรณ์
doก็เป็นฟีเจอร์ที่คิดถึงมาก- แก่นสำคัญไม่ได้อยู่ที่
doเอง แต่อยู่ที่รูปแบบสัญกรณ์ที่แม้เพิ่ม binding แล้วระดับ indentation ก็ไม่เพิ่มขึ้นเรื่อย ๆ flat_mapแบบซ้อนกันใน Ruby ทำให้ indentation ลึกขึ้นทุกครั้งที่เพิ่มแกน- สัญกรณ์
doของ Haskell แสดงการประกอบแบบเดียวกันได้อย่างแบนราบ
- แก่นสำคัญไม่ได้อยู่ที่
- สัญกรณ์
doทำให้สามารถฝัง structured concurrency สไตล์async/await, ธุรกรรมพร้อมกัน และ โปรแกรมเชิงความน่าจะเป็น ด้วยไวยากรณ์เบา ๆ แบบเดียวกัน
ควรเรียนหรือควรใช้ Haskell หรือไม่
- คำตอบต่อคำถาม “ควรเรียนภาษานี้ไหม ควรใช้ภาษานี้ไหม” ขึ้นอยู่กับเป้าหมาย
- หากเป้าหมายคือการเป็นโปรแกรมเมอร์ที่ดีขึ้น Haskell มีคุณค่าสูงในการเรียน
- Haskell เป็นภาษาที่น่าสนใจและทรงพลัง
- ประสบการณ์การเรียน Haskell ถูกประเมินว่าช่วยยกระดับความสามารถในการเขียนโปรแกรม
- หากเป้าหมายคือการใช้งานจริง ต้องระมัดระวังมากขึ้น
- ข้อดีของ Haskell มีอยู่จริง
- ต้องนำไปเทียบกับว่าบุคคลหรือองค์กรยอมรับแรงเสียดทานที่กล่าวมาข้างต้นได้มากเพียงใด
- ยังมีบุคคลและองค์กรที่ได้ประโยชน์จาก Haskell มากพอจนทำให้ความไม่สะดวกเล็ก ๆ เหล่านั้นเป็นเพียงบาดแผลเล็กน้อย
- หากต้องการคำตอบชัดเจนว่าใช่หรือไม่ คำตอบจะใกล้กับ “อย่าใช้ Haskell”
- หากสร้างองค์กรวิศวกรรม ผู้เขียนตัดสินว่าจะไม่ตั้ง Haskell เป็นภาษาหลัก
- เพราะรู้ทั้งคุณค่าของเครื่องมือที่ดีและต้นทุนของเครื่องมือที่แย่
- ชอบภาษาที่ให้ภาษาที่เสถียร ระบบนิเวศโค้ดที่ชัดเจน และเครื่องมือยอดเยี่ยมได้ทันทีมากกว่า
- ถึงอย่างนั้น Haskell ก็ให้เครื่องมือที่ยอดเยี่ยมเพื่อความปลอดภัยและความถูกต้องของโปรแกรม
- ในบางบริบท อาจคุ้มค่าที่จะยอมรับเครื่องมือที่ไม่ดีและ linting เพื่อกัน type family ที่ไม่จำเป็น
- Haskell ไม่ใช่ภาษาที่เหมาะกับผู้เขียนเป็นการส่วนตัวอีกต่อไป แต่ข้อสรุปคือ ถ้า Haskell ไม่เคยมีอยู่ในโลก โลกนี้คงแย่กว่านี้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ผมใช้ Haskell มาราว 10 ปี และเพิ่งเลิกใช้เป็นเครื่องมือทำมาหากินเมื่อไม่นานนี้ แต่เห็นด้วยอย่างชัดเจนกับประเด็นแรกของผู้เขียน: คอมมูนิตี้ Haskell ให้ความสำคัญกับการเรียนรู้อย่างมาก
การได้ทำงานกับคนที่อยากรู้อยากเห็นและมีเรื่องให้สอนและเรียนรู้อยู่เสมอเป็นเรื่องดี แต่คอมมูนิตี้นี้ไม่ค่อยเก่งเรื่องการทิ้งไอเดียที่ลองไปแล้ว ดังนั้น codebase Haskell ในระดับมืออาชีพ หากไม่ควบคุมอย่างเข้มงวด ก็มีแนวโน้มจะสะสม “สิ่งที่ทำได้ แต่ไม่ควรใช้” ไว้จำนวนมาก
อย่างไรก็ตาม ผมเห็นต่างเรื่อง toolchain เครื่องมือของ Haskell ไม่ค่อยดีจริง แต่พอลองใช้ Python, JS, Java, Rust, Elm ก็พบว่าเครื่องมือส่วนใหญ่ก็ไม่ค่อยดีเหมือนกัน และกลับคิดถึง toolchain ของ Haskell ด้วยซ้ำ cargo นั้นยอดเยี่ยม แต่ค่อนข้างเป็นข้อยกเว้น และก็สงสัยว่า Rust จะยังเลี่ยงสภาพนี้ได้หรือไม่เมื่อมันเก่าแก่ขึ้น
มีโปรเจกต์หนึ่งที่ทำเป็นครั้งคราว และมั่นใจ 100% ว่าครั้งหน้าที่เปิดขึ้นมาก็จะรันไม่ได้อีก การจัดการ dependency และ virtual environment ของ Python ก็แย่พอ ๆ กัน แต่ Poetry ค่อนข้างดี ส่วน Ruby ไม่ว่าจะใช้ chruby/rbenv/rvm ตัวไหนก็ทำงานได้ดีใกล้เคียงกัน และ Bundler ก็ทำงานได้ราบรื่น Java แม้จะมีข้อบกพร่อง แต่ประสบการณ์ IDE อยู่ระดับแนวหน้า และถึงการ refactor จะไม่ปลอดภัยเท่า Haskell แต่ก็ทำได้ง่ายมาก
Bundler โดยรวมไม่เลว และแม้จะมีการตัดสินใจด้านการออกแบบบางอย่างที่ไม่เห็นด้วย แต่ถ้าเดินตามแนวทางที่ถูกต้อง หลายส่วนก็เข้าที่เข้าทางได้ดี เครื่องมือของภาษาก็สามารถทำให้อยู่ในรูปแบบที่พังน้อยพอได้ และแม้ผมไม่ได้ใช้ cargo โดยตรงมากนัก แต่ถ้ามันดีที่สุดในกลุ่มจริง ๆ ก็ไม่น่าแปลกใจถ้ามันจะรักษาสภาพนั้นไว้ได้
ในแง่เครื่องมือ Haskell มีอย่างหนึ่งที่เท่าที่รู้ ภาษาอื่นแข่งด้วยได้ยาก: Hoogle
Hoogle ยอดเยี่ยมอย่างน่าทึ่ง แค่บอกสิ่งที่ต้องการด้วยภาษา Haskell มันก็จะบอกสิ่งที่ทำได้ใน Haskell เคยมีความพยายามคล้ายกันใน Rust และเคยพยายามทำ Noogle สำหรับ Nim ด้วย แต่ในภาษาที่ “การส่งอาร์กิวเมนต์ให้ฟังก์ชัน” กับ “การเรียกฟังก์ชัน” แยกกันชัดเจน มันจะทำงานแบบเดียวกันไม่ได้
ทุกครั้งที่เห็นโค้ด Rust ที่มี
Result,Boxพัวพันกัน ก็อยากให้มี Roogle ที่มีประโยชน์เท่า Hoogle สำหรับเครื่องมือ Haskell อื่น ๆ ไม่มีข้อดีอะไรนัก ส่วนใหญ่ดูเหมือนเป็นเพราะสัญชาตญาณของคอมมูนิตี้ที่อยากสร้างอะไร “นวัตกรรม” แทนที่จะปรับปรุงของเดิมยังไม่สมบูรณ์แบบ แต่เทียบกับเมื่อ 5 ปีก่อนแล้วดีกว่ามาก
อยากให้มีโปรเจกต์มากขึ้นเลือกแนวทางแบบนี้
ผมมองว่าปัญหาใหญ่ที่สุดคือ GHC ถูกผูกไว้กับเวอร์ชันเฉพาะของ
baseซึ่งเป็น standard libraryเมื่อ
baseเปลี่ยน และ GHC เวอร์ชันใหม่รองรับเฉพาะเวอร์ชันนั้น ถ้าจะใช้ GHC ที่ใหม่ขึ้นก็แทบต้องเปลี่ยน dependency ทั้งหมด ผมยังไม่เข้าใจว่าทำไมโค้ดที่คอมไพล์ด้วย GHC 9.6 จึงต้องใช้base4.18.0.0 ทำไมไบนารี GHC ต้องสนใจว่าโมดูลData.Listของโค้ดเป้าหมายที่คอมไพล์เป็นเวอร์ชันไหนผมเข้าใจว่าส่วนเฉพาะของ GHC ที่
baseเปิดเผยออกมาจะต้องผูกกับ GHC เวอร์ชันใดเวอร์ชันหนึ่ง แต่ไม่แน่ใจว่าทำไมส่วนที่เหลือต้องเป็นแบบนั้นด้วย อย่างไรก็ตาม เข้าใจว่ากำลังมีงานแยกbaseออกเป็นหลายแพ็กเกจเพื่อแก้เรื่องนี้: https://gitlab.haskell.org/ghc/ghc-wiki-mirror/-/blob/master...baseเก่าว่าไม่ถูกต้องได้baseต่างกันส่ง instance ของData.Listไปมาระหว่างกัน ก็จะเกิดปัญหาตัวอย่างเช่น หากมีลิสต์ของ
Data.Listอยู่ จะเอาผลลัพธ์จากฟังก์ชันของสองไลบรารีที่ต่างกันมาต่อท้ายลิสต์นั้นได้หรือไม่baseที่ติดตั้งซ้ำได้ เป็นเป้าหมายที่ทุกคนต้องการ สถานการณ์ตอนนี้ส่วนใหญ่เกิดจากเหตุผลทางประวัติศาสตร์ และการที่องค์ประกอบพื้นฐานซึ่งผูกกับภายในคอมไพเลอร์ถูกจับรวมกับองค์ประกอบที่ไม่ใช่พื้นฐาน หากลงแรงมากพอก็สามารถทำให้ดีขึ้นได้หากมีเหตุผลใหญ่ ๆ สามข้อที่ทำให้ความสนใจใน Haskell ค่อย ๆ จางลง ก็คือ ความชอบของใหม่ในเชิงสไตล์ ที่ยกย่องโค้ดอ่านยากจนทำให้บำรุงรักษาลำบาก, เครื่องมือที่ใช้งานไม่คล่องจนทำให้งานประจำวันช้าลง และการเปลี่ยนแปลงต่อเนื่องที่แม้จะเกิดเป็นครั้งคราวแต่ก็ต้องคอยใส่ใจอยู่เรื่อย ๆ และทำให้ของพังเป็นระยะ
เป็นข้อสังเกตที่สมเหตุสมผล ผมเรียน Haskell เยอะมากช่วงปี 2010–2012 ตัวภาษาเองยอดเยี่ยม แต่เอกสารกับเครื่องมือรับมือยาก ชุมชนเคยไปจาก Cabal และ Cabal hell อันเลื่องชื่อ ไปใช้ Stack แล้วก็กลับมาที่ Cabal อีกครั้ง โดยรวมแล้วดีขึ้น
ขณะเดียวกัน ภาษาโปรแกรมอื่น ๆ ก็ซึมซับองค์ประกอบของ functional programming เข้าไปด้วย เช่น Java เพิ่ม Streams, ฟังก์ชัน, lambda, algebraic data types, records และ pattern matching แม้ไวยากรณ์จะไม่สง่างามเท่า Haskell แต่ก็มีแนวคิดพื้นฐานของ functional programming อยู่ในนั้น
แล้วพวกเขาก็สร้างหลุมแห่งความทุกข์ทรมานไม่รู้จบขึ้นมาเอง และพอทนได้เพียงเพราะลำแสงไม่ได้ต่ำพอให้เห็นสีที่แสงสร้างขึ้นบนต้นไม้นอกหน้าต่าง ผู้ที่มาจากภายนอกถูกฉีกทึ้งระหว่างสำเนียงที่ปรับตัวเข้ากับความมืดที่สร้างขึ้นเอง กับสำเนียงที่งอกงามได้ในแสงแต่หายใจไม่ออกเมื่ออยู่ที่นั่น จึงเสียสติไปอย่างรวดเร็ว
ถ้าชุมชนกลับมาใช้ Cabal อีก ก็สงสัยว่าผมพลาดอะไรไป
ด้วยเหตุผลเหล่านี้จึงย้ายไปใช้ F# และแทบไม่ได้หันกลับไปมองมากนัก บางครั้งก็คิดถึง higher-kinded types อยู่บ้าง แต่ F# ก็มี generics และพูดตรง ๆ คือมันบังคับให้เขียนโค้ดที่เรียบง่ายกว่าใน Haskell ในบางแง่ ซึ่งโดยรวมแล้วผลลัพธ์ดีกว่า
แต่ F# ไม่ได้ให้ความรู้สึกแบบ “เซียน” เหมือน Haskell มันคล้าย Haskell แต่เน้นงานจริงอย่างถึงที่สุด ผมใช้ F# ทำงานได้มากมายและใช้อย่างสนุก แต่บางครั้งก็ยังมองย้อนกลับไปอย่างละห้อยว่าทำไมกับ Haskell ถึงไปกันไม่รอด
มักสงสัยอยู่บ่อย ๆ ว่าถ้าไม่ใช้ Haskell จะพลาดอะไรไปหรือเปล่า แต่สำหรับเป้าหมายของผม ดูเหมือนคงไม่ใช่
การเรียน Haskell เหมือนพยายามถอดรหัสภาษาต่างดาว ถ้าคุ้นกับลูป if-else ธรรมดา ๆ และการกำหนดค่าตัวแปรง่าย ๆ ก็เตรียมปวดหัวได้เลย
monad ไม่ใช่สัตว์ประหลาดจากอวกาศ แต่เป็นแนวคิดนามธรรมแปลก ๆ ที่อาจทำให้เกาหัวและตั้งคำถามกับการเลือกเส้นทางชีวิต ไลบรารีก็เหมือนต้องคุ้ยร้านของมือสองที่ของว่างไปครึ่งร้านเพื่อหาสิ่งที่ต้องการ ส่วนเอกสารครึ่งหนึ่งก็อ่านเหมือนอักษรภาพ
ประสิทธิภาพในโลกจริงก็อาจทำให้ต้องนั่งคิดอยู่นานว่าทำไม lazy evaluation ถึงกินหน่วยความจำมากกว่าที่คาดและทำให้ช้าลง งานก็หาได้ยาก เว้นแต่จะเป็นบริษัทเฉพาะทางมาก ๆ ที่เดิมพันกับ Haskell เต็มตัว และการดีบักก็รู้สึกเหมือนงมหาเข็มในกองอักษรภาพเชิงฟังก์ชัน
ใช้ Scala มาตลอด และคิดว่ามันให้ข้อดีของทั้งสองโลกได้ สามารถให้เหตุผลเชิงพีชคณิตได้ และถ้าหลัง refactor แล้วยังคอมไพล์ผ่าน ส่วนใหญ่ก็มักทำงานได้
type inference กับ monad ก็ทำงานได้ดี และยังได้ประโยชน์จาก ecosystem ของ Java ด้วย ถ้ามันเริ่มลึกลับเกินไป ก็ยังแหกกฎแล้วเขียนให้ใกล้ Java ได้ อยากรู้ว่าคนอื่นคิดอย่างไร
แต่ sbt ทำให้แทบบ้า และน่าจะเป็นส่วนที่ไม่ชอบที่สุดในประสบการณ์พัฒนา
ผู้ดูแลและ maintainer ของ Haskell ดูเหมือนตั้งใจจริง ๆ ให้ภาษานี้เป็นมิตรกับ การวิจัย การทดลอง และการสำรวจเชิงวิชาการ
ตัวมันเองไม่ใช่เรื่องเสียหาย แต่เมื่อถึงจุดหนึ่งมันย่อมชนกับผลประโยชน์ของโปรแกรมเมอร์ที่อยากใช้ Haskell เป็นเครื่องมือที่ใช้งานได้จริงและเสถียร สิ่งที่ต้องการน่าจะเป็นความสามารถในการทำเครื่องหมายแยก track สำหรับของที่เป็นเชิงทดลองและล้ำหน้า ไม่ว่าจะเป็น pragma หรือการประกาศระดับแพ็กเกจ แค่ดูแพ็กเกจก็ควรรู้ได้ว่าจะดึงเข้ามาหรือไม่
แบบนั้นนักพัฒนาที่เน้นงานจริงก็จะนำนโยบายอย่าง “เราใช้เฉพาะ stable Haskell” มาใช้ได้ ขณะเดียวกันก็ยอมรับพลวัตของ “Avoid success at all costs” ที่ว่าถ้า Haskell ถูกนำไปใช้มากเกินไป ภาษาอาจติดอยู่กับแรงเฉื่อยและผลักดันแนวคิดล้ำหน้าไม่ได้ แต่ก็จำเป็นต้องมี track อย่างเป็นทางการให้ผู้พัฒนาทั่วไปอยู่กับ subset ของ Haskell ที่จำกัดกว่าและเสถียรกว่าได้
Java จะไม่มีวันได้ของแบบนี้ https://www.stackage.org/
สุดท้ายแล้วความเรียบง่ายก็ขึ้นอยู่กับมุมมองของผู้ดู ฟีเจอร์ type หรู ๆ ก็มีกรณีใช้งานที่ทำให้ type สื่อความหมายได้มากขึ้น ทำให้โค้ดปลอดภัยขึ้น และแม้กระทั่งเรียบง่ายขึ้นได้ หากเรา internalize วิธีการทำงานของมันแล้ว
เป็นบทความที่ค่อนข้างดี แต่จุดที่ยังอ่อนคือ ผมไม่เคยรู้สึกถึงแรงกดดันให้ต้องใช้ฟีเจอร์ type ใหม่ ๆ หรู ๆ บางอย่าง ถ้าไม่อยากใช้
ถ้าไม่ชอบ HTTP API ระดับ type ของ Servant ก็ลดลงไปอีกชั้นแล้วใช้ warp ก็ได้ ไลบรารีต่าง ๆ เข้าใจว่า type ที่ซับซ้อนเกินไปอาจกลายเป็นกับดักได้ จึงมักจัดเป็นชั้น ๆ แบบนี้ จำเป็นต้องมีสัญชาตญาณว่าควรไปไกลแค่ไหน และในกระบวนการนั้นก็มีความผิดพลาดรวมอยู่ด้วย
ผมลองอ่าน เอกสารของ Servant แล้ว แต่ก็เป็นไปตามคาด คือมันตั้งสมมติฐานว่าผู้อ่านเข้าใจ Haskell มากกว่าที่ผมมีอยู่
บทความนี้โดนใจจริง ๆ ผมใช้ Haskell กับแอปพลิเคชันที่ค่อนข้างเป็นงานอุตสาหกรรมมาประมาณ 1.5 ปี และก่อนหน้านั้นก็มีประสบการณ์พอสมควร เช่น https://github.com/mattgreen/hython
ในรอบอาชีพ 20 ปี นี่เป็นครั้งแรกในระยะยาวที่ต้องเรียนรู้อะไรค่อนข้างมาก ซึ่งเป็นผลจากการรวมกันของโดเมนกับภาษา มันทั้งน่าตื่นเต้นและเหนื่อยล้า และอาจเป็นก้อนใหญ่พอสมควรเมื่อรวมกับชีวิตครอบครัวที่ยุ่งอยู่แล้ว
ถึงอย่างนั้นภาษานี้ก็ยอดเยี่ยม ผู้จัดการมักแนะนำให้ใช้แบบ top-down ซึ่งหมายถึงให้คิดให้ลึกว่าควรให้อ่านอย่างไรแล้วเริ่มจากตรงนั้น เพราะความยืดหยุ่นของ Haskell ถ่ายทอดเจตนาของนักพัฒนาได้ดี นี่เป็นการเปลี่ยนวิธีคิดครั้งใหญ่เมื่อเทียบกับภาษาส่วนใหญ่ที่พิมพ์
functionซ้ำ ๆ จนสมองเข้าสู่โหมดประหยัดพลังงาน และรู้สึกจริง ๆ ว่ามันถูกออกแบบมาให้เป็นมิตรต่อการเขียนแต่แม้จะจัดการ effect ที่ควบคุมได้ด้วย monad ไวยากรณ์ก็ยังอ่านกวาดตายากอยู่ดี ไม่แน่ใจว่าเป็นเพราะหนึ่งบรรทัดอาจอัดแน่นเกินไปหรือเปล่า ผมคิดถึง early return มาก และก็คิดถึง LSP ที่ยอดเยี่ยมของ Rust ด้วย Haskell เพิ่งเสียฟีเจอร์ช่วยเติม pattern match ไปเมื่อไม่นานมานี้เพราะการเปลี่ยนแปลงภายในของเวอร์ชัน 9.x และการซ้อน monad ก็ยังเป็นสิ่งที่ไม่ชอบและทำผิดบ่อย เวลา compile อาจโหดร้ายมาก โดยเฉพาะเมื่อเจอไลบรารี lens
ผมก็ไม่ชอบ lazy evaluation ที่เป็นค่าเริ่มต้นด้วย ชุมชนเองก็ยอมรับในระดับหนึ่งว่าเพราะค่าเริ่มต้นนี้ โปรแกรม Haskell จึงเสี่ยงต่อ space leak มากกว่าอย่างเห็นได้ชัด และในหลายโปรแกรมอาจตรวจไม่พบอยู่นานพอสมควร ในฐานะโปรแกรมเมอร์ระบบ เรื่องนี้ทำให้อยากกรีดร้อง
ถึงอย่างนั้น ชุมชนนี้ก็เป็นหนึ่งในกลุ่มโปรแกรมเมอร์ที่แข็งแกร่งที่สุดเท่าที่ผมเคยเห็นมา ผมไม่ได้อยากพูดย้ำมีมว่าพวกเขาฉลาดแค่ไหน แต่ประเด็นใหญ่คือพวกเขาคิดลึกจริง ๆ และผลักดันไปข้างหน้าอย่างจริงจัง สำหรับโดเมนของเราอย่างการวิเคราะห์โปรแกรม Haskell เข้ากันได้ราวกับถุงมือพอดีมือ แต่ในด้านอื่น ๆ ผมยังค่อนข้างระวังอยู่ โดยพื้นฐานแล้วผมยังโหยหาภาษาโปรแกรมที่เป็น OCaml แต่มีไวยากรณ์ดีกว่า
ไม่จำเป็นต้องเปิดเผย ExceptT ใน interface ก็ได้ ใช้เฉพาะภายในเพื่อ early return แล้วให้ฟังก์ชันคืนค่า Either ก็พอ
น่าเสียดายที่ท้ายที่สุดมันไม่ได้รับ traction มากนัก
มันผสาน object-oriented กับ functional programming ได้ดีกว่า OCaml และโค้ด OCaml แบบ idiomatic ก็คล้ายพอที่จะย้ายมาได้ง่ายด้วยการแปลงแบบตรงไปตรงมา