- คุณลักษณะของภาษาและระบบนิเวศของ OCaml นั้นยอดเยี่ยม และเหมาะทั้งกับโปรเจกต์ส่วนตัวและงานระดับมืออาชีพ
- มีการผสานหลายพาราไดม์และความสามารถขั้นสูงอย่างมั่นคง เช่น ระบบชนิดแบบสแตติก, ชนิดพีชคณิต, ระบบโมดูล, โมเดลเชิงวัตถุ, และ user-defined effects
- มี toolchain ที่สุกงอมแล้ว เช่นตัวจัดการแพ็กเกจ OPAM, ระบบบิลด์ Dune, การรองรับเอดิเตอร์ LSP/Merlin, และเครื่องมือทำเอกสาร Odoc พร้อมระบบนิเวศไลบรารีที่หลากหลายทั้งเว็บ บล็อกเชน และทูลลิง
- ชุมชนมีทั้ง การเข้าถึงง่าย ความเป็นมิตร และความเป็นมืออาชีพ ทำให้เหมาะกับการเรียนรู้และการทำงานร่วมกัน และยังมีแนวโน้มที่ดีในอนาคตจากการพัฒนาอย่างต่อเนื่อง
เหตุผลที่เลือก OCaml เป็นภาษาหลัก
- ผู้เขียนใช้ภาษาโปรแกรมมิงมาหลากหลายเป็นเวลานาน และในบรรดานั้นได้เลือก OCaml เป็นภาษาหลัก
- จุดเด่นที่สุดของ OCaml คือ ระบบชนิดแบบสแตติกที่ทรงพลัง และการรองรับ การเขียนโปรแกรมเชิงฟังก์ชัน ที่ยอดเยี่ยมเมื่อเทียบกับ C หรือภาษาเชิงฟังก์ชันอื่น ๆ
- ระบบชนิดดังกล่าวช่วยให้ป้องกัน บั๊ก ได้จำนวนมาก และช่วยให้ได้ประสบการณ์ด้านการปรับแต่งโค้ดให้เหมาะสมยิ่งขึ้น
- ผู้เขียนเคยใช้ OCaml ใน โปรเจกต์พัฒนา หลายงานจริง และพบว่า ประสิทธิภาพการทำงาน กับ ความเสถียร ดีขึ้นอย่างมาก
จุดเด่นของ OCaml และการใช้งานจริง
- โค้ดส่วนใหญ่เขียนได้รวดเร็ว และการใช้ การประกอบฟังก์ชัน กับ ข้อมูลไม่เปลี่ยนแปลง ช่วยเพิ่มความปลอดภัย
- ในช่วงหลัง ระบบนิเวศ และ เครื่องมือ (IDE, ระบบบิลด์ ฯลฯ) ของ OCaml ก็พัฒนาอย่างต่อเนื่อง
- ด้วยไลบรารีและแพ็กเกจภายนอกที่หลากหลาย ทำให้ การพัฒนาอย่างมีประสิทธิภาพ ในงานจริงเป็นไปได้มากขึ้น
- เมื่อเทียบกับ Python หรือ Java แล้ว OCaml อาจไม่เป็นที่รู้จักมากนัก แต่เป็นตัวเลือกที่แข็งแกร่งมากในด้าน ประสิทธิภาพการทำงาน ความปลอดภัย และความยืดหยุ่น
คุณลักษณะของภาษา
- มีรากฐานจากงานวิจัยผสานกับการใช้งานในอุตสาหกรรม จึงพัฒนาฟีเจอร์โดยเน้น ความสามารถในการแสดงออก และ ความปลอดภัย
- ฟีเจอร์สมัยใหม่ เช่น user-defined effects และ affine sessions
- การตรวจสอบชนิดแบบสแตติก เป็นทั้งตาข่ายนิรภัยและเครื่องมือออกแบบ ช่วยลบความเข้าใจผิดที่เกิดจากประสบการณ์ด้านชนิดที่ไม่ดี
- หลายพาราไดม์: เชิงฟังก์ชัน เชิงคำสั่ง เชิงโมดูล เชิงวัตถุ และรองรับมัลติคอร์
- ไวยากรณ์ตระกูล ML กระชับและสม่ำเสมอ และยังมีไวยากรณ์ทางเลือกอย่าง ReasonML
- ชนิดพีชคณิต (ผลคูณ ผลรวม และเอกซ์โปเนนเชียล) พร้อม pattern matching และพหุสัณฐาน ทำให้เด่นด้านการสร้างแบบจำลองข้อมูล/โดเมน
- ระบบโมดูล รองรับการแยกอินเทอร์เฟซ/อิมพลีเมนเทชัน การนามธรรม การใช้ซ้ำ และพหุสัณฐานขั้นสูง
- dependency inversion: มีวิธี inject ที่ยืดหยุ่นผ่านโมดูล/เอฟเฟกต์
ระบบนิเวศและทูลลิง
- เป้าหมายการคอมไพล์: native, bytecode, JavaScript(
Js_of_ocaml,Melange), WebAssembly - แนวทางการเขียนไลบรารีแบบหลายบริบทผ่าน MirageOS
- OCaml Platform:
- OPAM: การจัดการเวอร์ชัน สวิตช์ ดัชนีแพ็กเกจ และรองรับ CI
- Dune: บิลด์รวดเร็ว คอนฟิกแบบ S-expression และทำให้การเผยแพร่ง่ายขึ้นผ่าน
dune-release - LSP/Merlin: เติมโค้ดอัตโนมัติ นำทางโค้ด และจัดรูปแบบใน VSCode, Emacs เป็นต้น
- Odoc: รองรับ cross-reference, manual pages, doctest เป็นต้น
- ไลบรารีที่หลากหลาย: เว็บ (Dream, Ocsigen), บล็อกเชน·วิทยาการเข้ารหัส (HACL*), การทดสอบ (alcotest, qcheck เป็นต้น)
- มาตรฐานไลบรารีมีขนาดเล็ก แต่มีทางเลือกอย่าง Batteries, Base/Core, Containers
ความท้าทายใหม่และชุมชน
- ชุมชน OCaml แม้จะเล็ก แต่ เติบโตอย่างต่อเนื่อง และมีแนวโน้มที่เป็นมิตรต่อผู้ใช้
- สำหรับนักพัฒนาที่ต้องการความท้าทายจากภาษาใหม่หรือพาราไดม์ใหม่ OCaml เป็นภาษาที่คุ้มค่าแก่การเรียนรู้อย่างลึกซึ้ง
- ผู้ใช้จำนวนมากกล่าวว่าประสบการณ์กับ OCaml ช่วยเปิด มุมมองใหม่ และเพิ่ม ความสามารถในการแก้ปัญหา
บทสรุป
- OCaml เป็น ภาษาโปรแกรมมิงที่ทรงพลัง ซึ่งไม่ได้จำกัดอยู่แค่บางโดเมนเฉพาะ (เช่น การเงิน คอมไพเลอร์ การพัฒนาระบบ) แต่ใช้งานได้อย่างอเนกประสงค์
- ประสิทธิภาพ การบำรุงรักษา และ ความสามารถในการป้องกันปัญหา ที่ได้จากการใช้งานจริง เป็นสิ่งที่พิสูจน์คุณค่าของมันในสถานที่ทำงานจริง
- แม้อาจเป็นที่รู้จักน้อยกว่าภาษาใหม่หรือกระแสล่าสุด แต่หากให้ความสำคัญกับความน่าเชื่อถือและความปลอดภัย ก็เป็น ตัวเลือกที่ควรพิจารณาอย่างยิ่ง
2 ความคิดเห็น
เคยใช้ OCaml ตอนเรียนบัณฑิตศึกษาอยู่ช่วงหนึ่ง แต่ ecosystem ขาดแคลนมาก แหล่งอ้างอิงก็แทบไม่มี และโดยเฉพาะอย่างยิ่งแทบไม่มีใครให้ถามเลย ตามเกณฑ์ส่วนตัวของผม ในเกาหลีน่าจะเรียกได้ว่าถ้าไม่ใช่ฝั่งสมาคมวิชาการด้านภาษาโปรแกรมก็แทบไม่มีคนใช้เลย COBOL อะไรแบบนี้อย่างน้อยก็ยังเคยได้ยินกัน แต่ OCaml นี่คงไม่เคยได้ยินกันมาก่อน...
ความคิดเห็นบน Hacker News
ฉันเคยดูงานบรรยายเกี่ยวกับประสบการณ์ของ Google ในการนำ Rust เข้าไปใช้ในทีม Android มีอยู่สองอย่างที่น่าประทับใจ: พวกเขาย้ายโปรเจ็กต์หลายตัวจาก Python ไปเป็น Rust ดังนั้นดูเหมือนว่าประสิทธิภาพคงไม่ใช่ประเด็นใหญ่นัก และฟีเจอร์ที่ผู้ใช้ Rust ชอบที่สุดกลับเป็นของพื้นฐานอย่าง pattern matching และ ADT (Algebraic Data Types) เพราะงั้นเลยรู้สึกว่าส่วนที่ Rust สร้างคุณูปการจริง ๆ ไม่ใช่ความสามารถเฉพาะตัวอย่าง lifetime แต่เป็นสิ่งที่ภาษา ML ในยุค 1990 มีให้แล้ว ถ้า OCaml แก้ปัญหาความไม่สะดวกอย่าง multicore ได้ตั้งแต่ราวปี 2010 ก็น่าจะดังได้พอ ๆ กับ Rust น่าเสียดายที่ OCaml ตกอยู่ในช่องว่างระหว่างวงการวิชาการกับภาคอุตสาหกรรม ขอเสริมอีกอย่างว่า integer 31 บิตใช้งานกับ bit operation แล้วไม่สะดวกในทางปฏิบัติ และในแง่ความสวยงามก็ไม่ชอบเครื่องหมาย semicolon คู่เลยจริง ๆ
ผมคิดว่า OCaml ตอนนั้นก็อยู่ในสภาพที่ดีมากแล้วนะ ในปี 2010 ผมใช้มันทำงานได้สบายกว่า Python มาก แค่ดูสิ่งที่ JaneStreet ทำได้ก็พอ สาเหตุใหญ่ที่สุดที่ OCaml ไม่ถูกรับไปใช้อย่างกว้างขวาง น่าจะเป็นเพราะมันไม่ได้ถูกสร้างหรือผลักดันในสหรัฐฯ เราอยากเชื่อว่าความนิยมของภาษาเกิดจากความเหนือกว่าทางเทคนิค แต่สุดท้ายมันก็เป็นเรื่องของกระแส Rust เองที่ประสบความสำเร็จในวงกว้างก็เพราะการประชาสัมพันธ์มหาศาลและกิจกรรมชุมชนที่เข้มข้น ถึงขั้นมีพนักงานเฉพาะทางคอยช่วยผลักดันชื่อเสียงมัน
Google พยายามอย่างมากที่จะทำให้รายชื่อภาษาที่อนุญาตให้ใช้กับโค้ดโปรดักชันจริงสั้นที่สุดเท่าที่จะเป็นไปได้ ดูเหมือนว่า Rust จะถูกเลือกเพราะเป็นภาษาที่ใช้แทนหรือเสริม C++ ได้ ส่วน OCaml คงยากที่จะอยู่ในตำแหน่งนั้น (อาจแทน Go ได้บ้าง แต่โอกาสน้อย) ดังนั้นเหตุผลหลักที่ Rust ถูกเลือกน่าจะเป็นเพราะมันเป็นภาษาอย่างเป็นทางการเพียงตัวเดียวที่มี ADT มากกว่าเรื่องที่ไม่ให้ความสำคัญกับความเร็วในการ build และก็ไม่แปลกที่ OCaml จะมาแทน Rust ไม่ได้ ภาษาที่มี GC นั้นมีอยู่แล้วหลายตัวอย่าง Go, Haskell เป็นต้น และราวปี 2010 ภาษาที่ expressive พอจะเล็งงาน bare metal ได้จริง ๆ ก็มีแค่ C++ เท่านั้น (ซึ่งก่อน C++11 และ C++17 ก็แย่กว่านี้อีก)
เห็นด้วยเต็มที่ ถ้า OCaml แก้ปัญหาเล็ก ๆ น้อย ๆ บางอย่างได้ มันอาจกลายเป็นผู้เล่นสำคัญจริง ๆ ก็ได้ ความเร็วในการ build ตอนนี้ก็ยังเร็วกว่าของ Rust มาก แต่ OPAM (ตัวจัดการแพ็กเกจ) มีบั๊กบ่อยและขึ้นชื่อว่าใช้งานชวนสับสน การรองรับ Windows ก็แย่มากจนเข้าขั้นวิกฤต แย่กว่า Perl บน Windows ในอดีตเสียอีก เอกสารทางการก็สั้นเกินไปจนแทบใช้ประโยชน์ไม่ได้ ตัวไวยากรณ์เองก็ดูยาก และยังเจอข้อความลักษณะว่าไฟล์ครึ่งหนึ่งมี syntax error เพราะพิมพ์ผิดแค่นิดเดียวอยู่บ่อย ๆ ไวยากรณ์สไตล์ C ที่ Rust ใช้อยู่เดิมนั้นง่ายกว่ามาก สรุปคือข้อดีของ OCaml มีแค่ build เร็ว แต่แค่นั้นยังไม่พอจะเป็นเหตุผลให้หยิบมาใช้
เพราะอย่างนั้นเวลาผมอยากเขียนโปรแกรมแบบ ML ผมจะนึกถึง Kotlin, Scala, F# ก่อน Rust และทุกวันนี้แม้แต่ Java หรือ C# เองก็รับเอาองค์ประกอบแบบ ML ไปเยอะพอสมควรจนไม่รู้สึกขัดอะไรนัก ผมคุ้นกับ type system แบบ ML มาตั้งแต่ยุค Caml Light กับ Objective Caml แล้ว พอเห็นคนสมัยนี้คลั่ง Rust กัน ก็เหมือนกับว่าพวกเขาหลงคิดว่า Rust เป็นคนนำ type system แบบ ML กลับมาใหม่
สำหรับความเห็นที่ว่าอยากให้ OCaml เตรียมตัวมาดีกว่านี้ ผมกลับคิดว่าจุดแข็งที่สุดจริง ๆ คือการที่เรามีตัวเลือกภาษาให้ใช้หลากหลาย แม้แต่ในสหราชอาณาจักรเอง (แม้ประชากรจะน้อยกว่า) ก็มีภาษาหลากหลายอยู่ร่วมกันมาก ตัวอย่างเช่น Cornish ซึ่งเป็นภาษาตายของยุโรป ก็ถูกชาวท้องถิ่นช่วยกันฟื้นกลับมาในช่วงหลัง และในหมู่คนเลี้ยงแกะก็ยังมีภาษานับเลขชื่อ Cubric หลงเหลืออยู่ ผมเองก็เพิ่งเริ่มใช้โปรแกรมชื่อ Geneweb ที่พัฒนาด้วย OCAML เพื่อเก็บแผนผังตระกูลสำหรับคนรุ่นถัดไป (ย้ายมาจากแอป Windows ชื่อ TMG) มีข้อมูลครอบครัวอยู่ถึง 140,000 คน Geneweb ที่สร้างด้วย OCAML เลยทำให้ผมสนใจภาษานี้มากขึ้น ถ้าคุณรู้สึกว่าภาษาโปรแกรมมิงมันยาก ลองไปทำงานสาย family tree/genealogy ดู แล้วอีกไม่นาน GEDCOM จะทำให้คุณปวดหัวเอง
OCaml เป็นหนึ่งในภาษาที่ฉันรักมากที่สุด งานที่ทำกับมันมากที่สุดคือการสร้างแอป CRUD ให้ทีมจัดงาน Writer's Festival โดยใช้ OCaml (JSX บน ReasonML), Dream, HTMX และ DataTables แบบ 100% ฉัน reuse เทมเพลตฝั่งฟรอนต์เอนด์ด้วย module และพอมีการเปลี่ยนแปลงใน data model ตัวคอมไพเลอร์ก็ชี้ให้เห็นทันทีว่าจุดไหนพัง ซึ่งดีมาก ฉันยังย้ายข้อมูลจาก Excel ไปอยู่ใน DB ที่เป็นเรื่องเป็นราว และทำสิ่งต่าง ๆ ได้ใน ecosystem ของ OCaml มากเกินคาด ทั้งการส่งออกตารางงานจากเทมเพลตฟอร์แมต .odt หรือส่งออกเป็นไฟล์ zip ได้ตรงโดยไม่ต้องผ่านดิสก์ของเซิร์ฟเวอร์ แต่ก็มีข้อเสียคือ query DB ต้องเขียนเป็นสตริงทั้งหมด และการแปลง type ก็ต้องทำเองจนเหนื่อยมาก (ไม่มี type check ตอน compile time) ระบบยืนยันตัวตนก็ต้องทำเอง ทำให้เผลอใช้เวลาไปกับเรื่องที่ไม่ใช่การพัฒนาผลิตภัณฑ์หลักมากเกินไป หลังจากลองดูหลายภาษา สิ่งที่รู้สึกคือไม่มีภาษาที่สมบูรณ์แบบ ทุกภาษามีจุดอ่อนเฉพาะของตัวเอง ตอนนี้ฉันกำลังทำแอปใช้เองด้วย Rails ซึ่งมีของที่ต้องใช้เกือบทั้งหมดมาให้เป็นค่าเริ่มต้น เลยพอใจมากกว่าเพราะได้โฟกัสกับงานจริงอย่างการออกแบบเลย์เอาต์หรือการ deploy มากกว่าตัวภาษา
DarkLang เดิมพัฒนาด้วย OCaml ก่อนจะย้ายไป F# ในภายหลัง เหตุผลหลักคือ ecosystem ของไลบรารีและเรื่อง concurrency (บทความที่เกี่ยวข้อง) ผมคุ้นกับ .NET เลยอาจมีอคติอยู่บ้าง แต่ส่วนที่น่าเบื่อก็มีตัวเลือกเตรียมไว้ดี ทำให้โฟกัสกับปัญหาสาระสำคัญได้ ผมมีประสบการณ์ใช้ F# ในงานจริงพอสมควร และยังดูแล UI library ยอดนิยมอยู่ด้วย แต่เพราะ ecosystem ของภาษามีขนาดเล็ก แม้แต่บน .NET เองก็ไม่ได้แปลว่าจะมีคำตอบให้เสมอไปทันที ดังนั้นถ้าจะเลือกภาษานอกกระแส (เช่น F# แทน C#) ก็ควรจำไว้ว่ามันมีต้นทุน เช่นเดียวกับ OCaml ที่แม้จะเป็นภาษาทรงพลัง แต่เพราะอยู่นอกกระแสจึงมีความไม่สะดวกหลายอย่าง บางบริษัทก็ใช้มันในระบบจริงอยู่ แต่ก็มักเป็นเคสที่ตอบโจทย์ความต้องการเฉพาะของตัวเอง
ผมพยายามจะชอบ OCaml มาหลายปี แต่ส่วนที่อึดอัดที่สุดคือ “ไม่สามารถ print object แบบตามใจได้” แม้จะใช้ ppx เพื่อ derive ฟังก์ชัน
to_stringอัตโนมัติได้ แต่การตั้งค่ามันจุกจิก และประสบการณ์ใช้งานก็ด้อยกว่า Rust ถ้าจะพิมพ์ type อย่าง Set หรือ Map ก็ต้องทำงานเพิ่ม (ตัวอย่างอ้างอิง) ใน golang แค่ฟอร์แมต"%v"ก็พิมพ์เกือบทุกอย่างได้ง่าย ๆ แต่ OCaml ต้องลงแรงมากกว่าในเรื่องแบบนี้%vของ Go ก็ไม่ได้สมบูรณ์แบบเหมือนกัน ถ้าจะไล่ pointer ให้ลึกกว่านี้ก็ยังต้องใช้ไลบรารีอย่าง go-spew เพิ่มอยู่ดี วิธีแบบ__repr__ของ Python สะดวกที่สุดเท่าที่ผมเคยเห็นมาผมยังไม่เคยใช้ OCaml โดยตรง แต่ประสบการณ์ทำงานกับ F# นั้นราบรื่นมาก ในยุค LLM แบบทุกวันนี้ ผมคิดว่าภาษาฟังก์ชันน่าจะกลับมาได้รับความสนใจอีกครั้ง เพราะในพาราไดม์แบบฟังก์ชันอย่าง OCaml หรือ Haskell เราบีบอัดข้อมูลจำนวนมากลงในข้อความสั้น ๆ ได้อย่างมีประสิทธิภาพ บางทีนั่นอาจทำให้ใส่ความหมายได้มากขึ้นใน context window ของ LLM ก็น่าสนใจที่จะลองดูว่าจะใช้มันทำ refactor ที่ซับซ้อนกว่า Java, C#, Ruby ได้ในคราวเดียวหรือไม่
ตอนแรกผมก็คิดแบบนั้น แต่พอได้ทำงานกับโค้ดเบส Haskell ขนาดใหญ่จริง ๆ ความคิดก็เปลี่ยนไป ไม่รู้ว่าเพราะในชุดข้อมูลฝึกมี FP น้อยหรือเปล่า แต่ภาษาที่กระชับกว่าเหมือนจะไม่เข้ากับ LLM เท่าไร โค้ดที่ verbose กว่าทำให้ LLM มีโอกาสแก้ตัวเองหลังจากทำนาย token ผิดได้มากกว่า เลยรู้สึกว่ามันสร้างโค้ดที่ถูกต้องได้ดีกว่า
จากการทดลองส่วนตัว ผมลองทำเกม CLI ง่าย ๆ ด้วย C++ และ Haskell จำนวนบรรทัดของ Haskell น้อยกว่า แต่จำนวนคำแทบพอ ๆ กัน มันเลยแค่ “ดูกว้างกว่า” เท่านั้น ยังไม่ได้ลองเทียบกับ Java หรือภาษาที่ explicit กว่านี้ แต่ผมคิดว่าสไตล์ที่เหมาะสมก็ขึ้นอยู่กับลักษณะของโปรแกรม บางอย่างอาจเหมาะกับแบบ imperative มากกว่า บางอย่างก็เหมาะกับแบบ functional มากกว่า
ถ้าความสามารถในการสร้างโค้ดของ LLM พัฒนาไปอีกนิด ผมอยากเห็นการใช้ type system ที่แข็งแรงมากจริง ๆ กับ effect system เพื่อจำกัดขอบเขตพฤติกรรมของโค้ด เช่น ถ้ามี dependent types ก็อาจตรวจสอบได้ตั้งแต่ compile time ว่า “ฟังก์ชันนี้ต้องคืนค่าลิสต์ที่เรียงแล้วเสมอ” หรือ “ฟังก์ชันนี้ต้องคืนคำตอบ Sudoku ที่ถูกต้องเสมอ” ถ้าเพิ่ม effect system เข้าไปอีก ก็อาจระบุได้ว่า “ฟังก์ชันนี้คืนคำตอบ Sudoku ที่ถูกต้อง แต่จะไม่เข้าถึง network หรือ filesystem” ถ้า LLM ก้าวหน้าไปมากกว่านี้ บางทีระดับนี้อาจทำได้ใน Python เช่นกัน แต่ถ้าความก้าวหน้ายังช้า ผมมองว่าอนาคตอาจเป็นการเอา LLM ที่เชื่อถือไม่ได้มาครอบด้วยระบบ deterministic ที่เชื่อถือได้
ตอนใช้ cats-effect (ไลบรารี effect) ใน Scala ผมพบว่าความช่วยเหลือจาก LLM ทำให้ความเร็วในการพัฒนาเพิ่มขึ้นมหาศาล โค้ด cats-effect มักทำให้แม้แต่แนวคิดง่าย ๆ ก็ดูยาก แต่ถ้าแค่ถาม LLM ว่า “ถ้าอยากทำ ~ ใน cats-effect ต้องทำยังไง?” ก็แก้ได้เลยประมาณ 80% ที่เหลืออีก 20% ก็แค่ให้บริบทเพิ่ม ในมุมมองการดูแลรักษายังถือว่ากำลังทดลองอยู่ แต่ความรู้สึกท้อกับการเขียน functional programming แบบอิง effect ลดลงมาก รอบหน้าผมอยากลองดูว่า Claude Code จะทำได้ดีแค่ไหน
Haskell มีข้อได้เปรียบใหญ่สองอย่างสำหรับการสร้างโค้ดด้วย LLM อย่างแรกคือ type system ที่ expressive มากช่วยจับความผิดพลาดหลายแบบได้ ทำให้เอา compile error ที่เกิดขึ้นกลับไปเป็น feedback ให้ LLM ได้ อย่างที่สองคือการปรับปรุงโค้ดให้ดีขึ้นด้วย property-based testing (เช่น QuickCheck) ทำได้ง่ายและแม่นยำ LLM เองอาจเขียนตัวทดสอบได้ไม่เก่งนัก แต่ถ้าเราเติมให้เอง ก็ช่วยจับบั๊กในโค้ดที่มันสร้างขึ้นมาได้เร็วมาก
อ่านบทความนี้แล้วเหมือนเป็นการปิดประเด็นคำถามว่า “ทำไมไม่ใช้ F# แทน OCaml ล่ะ?” ในแทบทุกเธรดเกี่ยวกับ OCaml มักมีคนเสนอว่า “ใช้ F# สิ ปัญหาเรื่องเครื่องมือก็หายแล้วไม่ใช่เหรอ?” ผมเองก็สนใจ OCaml และเคยเห็นมันถูกเรียกว่า “Go with types” เลยรู้สึกอยากลอง แต่จนถึงตอนนี้ตัว OCaml เองก็ยังไม่ได้ดึงดูดผมแบบเต็มที่ มันให้ความรู้สึกต่างจากความกระตือรือร้นในชุมชนของภาษาอย่าง Erlang, Ruby, Rust, Zig
ผมกลับเป็นคนที่ย้ายมา OCaml เพื่อหนี ecosystem เครื่องมือของ F# ต่างหาก ตอนที่ผมใช้ F# คอมไพเลอร์ช้า ecosystem เน้น C# เป็นหลัก MSBuild ก็อ่อนและเอกสารน้อย Ionide ก็ล่มบ่อย Fantomas ก็เชื่อถือไม่ค่อยได้ ปัญหาเรื่องเครื่องมือมีเยอะเหมือนกัน แต่ OCaml เองก็ยังทดแทนฟีเจอร์ด้าน performance ของ F# ไม่ได้ทั้งหมด (เช่น value type และสิ่งที่ CLR รองรับ) ดังนั้นจนถึงตอนนี้ผมก็ยังหาภาษาในสาย ML ที่เรียบง่ายถูกใจไม่เจอ หวังว่า OxCaml อะไรทำนองนั้นจะช่วยแก้ได้ในอนาคต
ช่วงหลังผมไม่ได้ใช้ OCaml มากนัก แต่ตัวแก่นของภาษายังคงเป็นสิ่งที่ผมชอบที่สุด สไตล์การเขียนโค้ดของผมมีแนวโน้มจะไปรวมอยู่ในฟังก์ชันใหญ่ฟังก์ชันเดียว และใน OCaml มันช่วยบังคับให้หลีกเลี่ยงแบบนั้นได้อย่างเป็นธรรมชาติ ตอนนี้ผมใช้ Rust ใน side project อยู่ แต่จริง ๆ แล้ว OCaml สบายมือกว่า ด้วยเหตุผลข้างบนนี้เอง ผมเลยอยากลอง F# สักครั้งเหมือนกัน
มีคำถามเรื่องคำศัพท์: ในบทความเรียก function type ว่า “exponential types” แต่ผมยังไม่ค่อยเข้าใจว่าทำไม type ของ higher-order function ถึงถูกเรียกว่า exponential type
มีคำอธิบายดี ๆ ไปแล้ว แต่เหตุผลลึกกว่านั้นคือ function type ปฏิบัติตามกฎเลขยกกำลังในเชิงพีชคณิต ตัวอย่างเช่น A → (B → C) สามารถ curry ให้เป็น (A × B) → C ซึ่งเป็น isomorphic กัน นี่คล้ายกับ
(cᵇ)ᵃ = cᵇ˙ᵃและ(A + B) → Cก็ isomorphic กับ(A → C) × (B → C)ซึ่งตรงกับกฎcᵃ⁺ᵇ = cᵃ·cᵇที่จริง function type ชั้นหนึ่งก็เป็น exponential อยู่แล้ว เช่น sum type มีจำนวนค่าตามจำนวนเคส (ตัวอย่าง:
A of bool | B of bool→2+2=4แบบ) product type กับ exponential type ก็เช่นกัน ถ้าเขียนเป็นbool -> boolจะมีค่าที่เป็นไปได้2^2 = 4แบบ (ถ้าไม่คิดเรื่อง side effect)ปกติเวลาพูดถึง ADT (Algebraic Data Type) มักพูดกันแค่ sum กับ product เพราะฟังก์ชันไม่ใช่ data เลยไม่ค่อยถูกพูดถึง แต่ type แบบ
a -> bมีกรณีได้b^aแบบ จึงมองในแนวเดียวกันได้ผมก็สงสัยเหมือนกัน แต่ในเชิงคณิตศาสตร์ พอมีผลบวก (sum) กับผลคูณ (product) แล้ว ถัดไปก็คือเลขยกกำลัง (exponent) เลยคงเรียกแบบนั้นในเชิงอุปมา
ทุกคำตอบก็ถูกต้อง แต่จริง ๆ ใน category theory เขาเรียก function type ว่า “exponential product” ชื่อนี้ก็มีที่มาจากการที่จำนวนฟังก์ชันจาก A ไป B คำนวณได้เป็น cardinality ของ B ยกกำลัง cardinality ของ A
เคสของ sum-type เป็นค่า (
expression) ผ่าน type constructor ดังนั้นแน่นอนว่ามันมี type ตัวอย่างเช่นแต่ละเคสต่างก็มี type ของตัวเอง และด้วย pattern matching คุณก็ unpack พารามิเตอร์ของ type constructor ได้อยู่แล้ว ถ้าแยกเคสออกมาเป็น type ต่างหาก ข้อดีเรื่อง exhaustiveness (กันเคสตกหล่น) ของ sum-type จะหายไป และสุดท้ายจะเปิดทางให้แทนสถานะโปรแกรมที่ไม่ถูกต้องได้ sum-type ประกาศครั้งเดียวแล้วใช้ได้หลายครั้ง และโดยมากก็เป็นของใช้เฉพาะงาน ความอ่านง่ายของโค้ดก็สำคัญ ดังนั้นบางทีความยืดยาวอาจถูกประเมินต่ำไป ขอย้ำว่า C#/Java ไม่ได้รองรับ sum-type แบบแท้จริง ดูตัวอย่างข้างล่างแล้วจะเห็นว่า C# ซับซ้อนเกินจำเป็นเพราะแนวคิดแบบ OOP
ใน ML เขียนได้กระชับกว่ามาก
วิธีทั้งสองแทบเหมือนกัน แต่ส่วนประกอบแบบ OOP ของ C# กลับเป็นตัวขัดขวางเสียเอง
ใน OCaml คุณใช้ GADT, polymorphic variants ฯลฯ เพื่อทำให้แต่ละเคสดูเหมือนเป็นคนละ type ได้เช่นกัน แต่โดยทั่วไปการแยก sum-type ออกจะทำให้ generalize ยากขึ้นและเข้าใจยากขึ้น อีกทั้งยังพ่วงปัญหาเรื่อง type equality กับ variance มาด้วย
ผมไม่เข้าใจว่าทำไมต้องเถียงกันเรื่อง sum-type กับ sealed-type ด้วย ผมเองชอบภาษาฟังก์ชันมากกว่า แต่ด้วยความสามารถในการแยกแยะกันในระดับ type นั้น sealed type ก็จำลอง sum-type ได้ครบทั้งหมด และเพราะมี subtyping บางด้านก็ทำให้ทั้งการนิยามและการใช้งานง่ายกว่า แม้พาราไดม์ของระบบจะต่างกันมาก แต่ในเชิงคณิตศาสตร์ก็เกือบเทียบเท่ากัน และลูกเล่นทาง type ที่ทำได้ใน OOP/FP ก็แทบสร้างได้หมดตราบเท่าที่ภาษานั้นอนุญาต
ผมไม่เห็นด้วยว่าความยืดยาวของการประกาศ sum-type ใน Java/Kotlin คุ้มค่าที่จะยอมรับ มันให้ความรู้สึกเป็น boilerplate แบบภาษาบน JVM ตามสูตรมากกว่า
อยากให้มีคนที่รู้ syntax ของ ReasonML ดีขนาดนี้มาช่วยเปรียบเทียบข้อดีข้อเสียให้ละเอียดหน่อย (ในบทความมีพูดถึงสั้น ๆ)
สิ่งที่ผมเสียดายที่สุดคือ let binding (เอกสารทางการ) ใน ReasonML เราสามารถ custom operator อย่าง
>>=สำหรับ monad ได้เอง ทำให้จัดการได้ง่าย rescript (fork ของ ReasonML) ยังไม่มีสิ่งนี้ แต่กลับรองรับไวยากรณ์ async/await ได้ดี เลยช่วยกับโค้ด async มาก ส่วน Melange (ที่บทความพูดถึงสั้น ๆ) รองรับ let binding ใน syntax แบบ Reason เพราะฉะนั้นในฟรอนต์เอนด์ที่ใช้ React นั้น Reason ML ของ Melange จึงได้เปรียบมาก ด้วย let binding (ร่วมกับ JSX) ทำให้เขียนโค้ด async แบบ monadic ได้สะอาด ในไวยากรณ์ OCaml เองก็อ้อมด้วย PPX ได้ แต่ syntax highlighting ใน editor มักไม่ค่อยดี พอมองฝั่งแบ็กเอนด์ ผมชอบสไตล์ Python เลยยังติดใจกับวงเล็บปีกกาอยู่ และชอบการเรียก/นิยามฟังก์ชันแบบไม่ต้องมีวงเล็บ แต่ในฐานะผู้ใช้ OCaml มือใหม่ ผมก็ยังสับสนกับการใช้ argument ที่ไม่ใช่ตัวแปรจนรู้สึกยากอยู่ หวังว่าประสบการณ์นี้จะช่วยได้ผมแทบไม่ได้ใช้ ReasonML เลย เลยไม่ค่อยรู้สึกถึงข้อดีอะไร นอกจากข้อเท็จจริงที่ว่ามันตายไปแล้วถึงสองรอบในช่วง 4 ปี...
ผมอยากให้ syntax ของ Reason ถูกเผยแพร่กว้างกว่านี้ แต่ถ้าจะสื่อสารกับชุมชน OCaml ก็เรียน syntax มาตรฐานไปเลยดีกว่า เพราะโค้ดกับเอกสารส่วนใหญ่เป็น syntax มาตรฐาน สุดท้ายก็ต้องอ่านให้ออกอยู่ดี
สิ่งที่น่าหงุดหงิดที่สุดใน ReasonML ที่ผมเจอคือ LSP ทำงานไม่ค่อยได้เรื่อง
อยากให้ช่วยอธิบายส่วนที่ใช้ effects system ทำ dependency injection ให้ละเอียดกว่านี้ แนวคิดเรื่อง bind ค่าทดสอบ/ค่าโปรดักชันด้วย pattern matching ดูน่าสนใจ แต่จากตัวบทความยังนึกภาพไม่ออก และเพิ่งรู้เป็นครั้งแรกด้วยว่าระบบ module มี type system ของตัวเองอยู่ด้วย น่าสนใจมาก