เพราะเป็นปี 2023 ก็เลยต้องเรียน Common Lisp แน่นอน
(log.schemescape.com)- จากกระแสที่กำลังคิดถึงภาษาที่จะยังใช้ได้ในอนาคต ตัวเลือกครั้งนี้ใกล้เคียงกับการทดลองด้วยตัวเองกับ เวิร์กโฟลว์การพัฒนาที่เก่าแก่แต่ยังทันสมัย มากกว่าความคุ้มค่าเชิงปฏิบัติ
- Janet เป็นภาษาสคริปต์ขนาดเล็กสาย Lisp/Clojure ที่มุ่งไปยังพื้นที่คล้ายกับ Lua และดึงดูดความสนใจด้วยไลบรารีมาตรฐานและ เมตาโปรแกรมมิงบนพื้นฐานของมาโคร
- จุดเด่นของ Janet ที่ดูน่าสนใจอย่างความพกพาได้ ความสามารถในการฝังในโปรแกรมอื่น และ Parsing Expression Grammars นั้นสอดคล้องกับความต้องการปัจจุบันค่อนข้างจำกัด ขณะที่ตัวเลือกเดิมอย่าง CHICKEN Scheme, CLISP, SBCL และ Packrat ก็ดูเพียงพอแล้ว
- แก่นสำคัญของ การพัฒนาที่ขับเคลื่อนด้วย REPL ใน Common Lisp คือการสามารถตรวจดูและแก้ไขสแต็ก ตัวแปร และข้อมูลสดในสถานะที่เกิดข้อผิดพลาดระหว่างรันไทม์ แล้วกลับไปรันงานที่หยุดค้างต่อได้
- แม้จะมีต้นทุนในการเรียนภาษาใหม่และไลบรารีมาตรฐาน แต่ผู้เขียนเห็นว่าคุ้มค่าที่จะลองสัมผัสโดยตรงในสภาพแวดล้อมที่เวิร์กโฟลว์นี้ถูกออกแบบไว้ตั้งแต่ต้น
เหตุผลที่ความสนใจย้ายจาก Janet ไปยัง Common Lisp
- เคยครุ่นคิดถึง future-proof programming languages เพราะอยากเขียนโค้ดที่ยังใช้ได้ในอนาคต แต่เมื่อจะสร้างและแชร์อะไรบางอย่าง ก็ยึดท่าทีแบบปฏิบัตินิยม โดยเลือกภาษาที่ได้รับความนิยม พกพาได้ และใช้งานสะดวก
- ขณะเดียวกันก็สำรวจภาษาโปรแกรมและเครื่องมืออื่น ๆ เพื่อความสนุกและการทดลองด้วย
- ช่วงหลังได้อ่านหนังสือฟรี Janet for Mortals จึงเริ่มสนใจ Janet
- Janet เป็นภาษาสคริปต์ที่ได้รับแรงบันดาลใจจาก Lisp/Clojure ขนาดค่อนข้างเล็ก ซึ่งมุ่งไปยังช่องว่างคล้ายกับ Lua
- มีไลบรารีมาตรฐานจริง ๆ เมตาโปรแกรมมิงแบบ Lisp และการรันในช่วงคอมไพล์ผ่านมาโคร
- สิ่งที่ดึงดูดที่สุดใน Janet นอกจากความเป็น Lisp โดยทั่วไปคือ ความพกพาได้ ความสามารถในการฝังในโปรแกรมอื่น และ Parsing Expression Grammars
- ตอนนี้ยังไม่มีความจำเป็นต้องฝังภาษาไว้ในโปรแกรมอื่น ความสามารถในการฝังจึงไม่ได้เป็นเหตุผลชี้ขาด
- ด้านความพกพา CHICKEN Scheme, CLISP และ Steel Bank Common Lisp ก็ดูใช้ได้ดีเช่นกัน
- ส่วนการพาร์ส Packrat ก็ดูเป็นตัวเลือกที่สมเหตุสมผล
- สุดท้ายแล้ว เหตุผลที่ต้องเลือก Janet แทน Lisp เดิม ๆ จึงลดลง
เวิร์กโฟลว์ที่ขับเคลื่อนด้วย REPL ซึ่งทำให้เริ่มเรียน Common Lisp
- บทความ ที่พูดถึง เวิร์กโฟลว์ที่ขับเคลื่อนด้วย REPL ซึ่งแทบจะเป็นเอกลักษณ์ของ Common Lisp กลายเป็นแรงจูงใจในการเรียน
- สถานการณ์ตัวอย่างคือฟังก์ชัน
fooเรียกฟังก์ชันbarที่ยังไม่ได้ถูกนิยาม และเกิดข้อผิดพลาดระหว่างการรันfoo- ในสภาพแวดล้อม Lisp รุ่นเก่าหรือ Smalltalk การหยุดของ
fooจะนำไปสู่ breakloop - breakloop คือ REPL เต็มรูปแบบที่อยู่ภายในสภาพแวดล้อมแบบไดนามิกของฟังก์ชันที่ถูกหยุดไว้
- สามารถเลื่อนไปมาระหว่าง call stack ที่หยุดค้าง และตรวจดูตัวแปรที่มองเห็นได้ในแต่ละ stack frame
- สามารถตรวจสอบและแก้ไขข้อมูลสดของโปรแกรมที่กำลังรันอยู่ได้
- หากสาเหตุคือค่าตัวแปรหรือฟิลด์ที่ผิด ก็เปลี่ยนค่าแล้วกลับไปรันฟังก์ชันที่หยุดไว้ต่อได้
- สามารถนิยามฟังก์ชัน
barที่ขาดไปภายใน REPL แล้วกลับไปรันfooต่อเพื่อให้ได้ผลลัพธ์ที่สมเหตุสมผล
- ในสภาพแวดล้อม Lisp รุ่นเก่าหรือ Smalltalk การหยุดของ
- แม้เคยเห็นฟีเจอร์ “edit and continue” มาหลายแบบแล้ว แต่ประเด็นที่ Common Lisp ถูกสร้างขึ้นโดยคำนึงถึงเวิร์กโฟลว์ลักษณะนี้ทำให้รู้สึกใหม่
- ประสบการณ์ดีบักที่ผ่านมาเปลี่ยนจาก
printf debuggingไปสู่การใช้ดีบักเกอร์จริงprintf debuggingคือการใส่โค้ดบันทึกล็อกชั่วคราว คอมไพล์ใหม่ แล้วตรวจดูผลลัพธ์ที่รันได้- ในสถานการณ์ที่แก้ไขหรือรันโปรแกรมใหม่ไม่ได้ ก็จำเป็นต้องเรียนรู้ดีบักเกอร์
- ดีบักเกอร์จริงเป็นเครื่องมือที่ถูกต้อง แต่การตั้งค่าสภาพแวดล้อมสำหรับดีบักอาจเจ็บปวด หรือบางครั้งอาจทำไม่ได้เลย
- Common Lisp ดูเหมือนเป็นสภาพแวดล้อมที่ผลักการดีบักไปอีกขั้น
- เคยมีประสบการณ์แก้หน่วยความจำในดีบักเกอร์เพื่อทดสอบแนวทางแก้ไขที่เป็นไปได้
- แต่สิ่งที่ดึงดูดเป็นพิเศษคือการเขียนโค้ดใหม่และแพตช์โค้ดได้ภายในโปรเซสที่กำลังรันอยู่
- เป้าหมายของการเรียน Common Lisp คือการตรวจสอบด้วยตัวเองว่าวิธีนี้เป็นรูปแบบการทำงานที่สนุกหรือไม่ ขณะ สร้างโปรแกรมแบบโต้ตอบ
- ยังไม่แน่ใจว่าการเรียนภาษาใหม่และไลบรารีมาตรฐานใหม่เพื่อสำรวจเวิร์กโฟลว์ใหม่เป็นทางเลือกที่ดีที่สุดหรือไม่ แต่ก็ยังไม่เห็นทางเลือกที่ดีกว่าอย่างชัดเจน
- ภาษาอื่นอาจมีการผสาน REPL กับเอดิเตอร์ในลักษณะคล้ายกัน
- แต่ผู้เขียนมองว่าฟีเจอร์เหล่านั้นไม่ได้ถูกสนับสนุนมาตั้งแต่ต้น หากแต่ถูกเติมเข้ามาภายหลัง จึงอาจมีบั๊กมากกว่า
- แม้สุดท้ายจะไม่พอใจ อย่างน้อยก็ยังถือว่าได้ลองภายใต้เงื่อนไขที่ดีที่สุดด้วยเครื่องมือมาตรฐานแล้ว
- แหล่งข้อมูลที่ช่วยในการเรียน Common Lisp ถูกรวบรวมไว้ในบทความแยก Common Lisp resources
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
สิ่งที่ชอบใน Common Lisp จนถึงตอนนี้คือ ระบบ condition ที่เรียบร้อย และสามารถควบคุมโค้ดที่อยู่ไกลออกไปได้ง่ายด้วย restart ส่วน การพัฒนาแบบขับเคลื่อนด้วย REPL ก็สะดวกในสถานการณ์ที่ยังไม่ค่อยรู้ว่าจะเกิดอะไรขึ้น เช่น ตอนพาร์สแหล่งข้อมูลที่ไม่คุ้นเคย เพราะสามารถแก้โค้ดแล้วทำต่อได้โดยไม่เสียบริบท
มี implementation หลายตัวและทำงานร่วมกันได้ดีด้วย ในกรณีหนึ่งจึงสามารถสลับใช้ SBCL เพื่อความเร็ว กับ CLISP เพื่อลดการใช้หน่วยความจำได้ หนึ่งในเหตุผลที่ทำให้เอียงมาทาง CL มากกว่า Scheme ตอนเรียน Lisp ก็คือมี implementation ที่เข้ากันได้หลายตัว
แม้จะเป็นมือใหม่ Emacs แต่การผสานกับ Common Lisp ก็ยอดเยี่ยม และยังทำงานได้ดีบนเน็ตบุ๊กที่ช้ามากซึ่งใช้พัฒนาอยู่ ทุกวันนี้ในยุคที่มีคอมพิวเตอร์เร็ว ๆ, VS Code และ language server ข้อดีนี้อาจไม่ใหญ่เท่าเดิม แต่ให้ความรู้สึก retrofuture ชัดเจน
มีจุดที่น่าเสียดายอยู่เหมือนกัน ตัวจัดการแพ็กเกจที่นิยมที่สุดอย่าง QuickLisp ก็ใช้ได้ แต่ยังไม่ถึงระดับฟีเจอร์ที่คุ้นเคยใน ecosystem ภาษาใหม่ ๆ ตัวภาษาเองเหมือนถูกตรึงไว้ในกาลเวลา ทำให้หลายอย่างอย่าง thread, synchronization, argument บรรทัดคำสั่ง ฯลฯ ต้องพึ่งไลบรารีสำหรับการทำงานร่วมกัน และถ้า SBCL รองรับ การ build แบบ static ทั้งหมด เพื่อให้แจกจ่าย binary ไปยัง Linux distribution ที่ไม่ใช่ glibc ได้ ก็คงดีมากจริง ๆ
ถ้างานสำคัญอย่างการดึงซอร์สโค้ดไม่รองรับการเข้ารหัสหรือการยืนยันตัวตน ผมมองว่า QL ก็ยากจะเป็นตัวเลือกสำหรับคนที่ใส่ใจด้านความปลอดภัย
อีกเรื่องคือ SBCL ถูกโฮสต์บน SourceForge ก็เป็นปัญหา SourceForge เคยฝังมัลแวร์ลงใน archive ดาวน์โหลดในอดีต จึงมองว่านี่ก็เป็นประเด็นความปลอดภัย ทุกวันนี้ดูเหมือนไม่มีเหตุผลสมควรให้ต้องใช้ SourceForge ต่อไป และก็ไม่ค่อยเข้าใจว่าทำไมโปรเจกต์สำคัญ ๆ ยังอยู่ที่นั่น
ผมชอบ Lisp และโดยเฉพาะ Common Lisp มากจริง ๆ แต่สิ่งเหล่านี้ทำให้ถอยห่างออกมา และดูเหมือนไม่ค่อยมีความสนใจจะแก้ไขด้วย
https://www.timmons.dev/posts/static-executables-with-sbcl.h...
https://www.timmons.dev/posts/static-executables-with-sbcl-v...
https://cons.io
Gerbil/Gambit Scheme เป็นทางเลือกแทน CL ที่สามารถสร้าง binary แบบ static ทั้งหมดได้
ใช้ได้ทั้งเป็นไลบรารี Quicklisp ปกติ, เป็น core image ที่เริ่มได้ทันที หรือเป็น binary ก็ได้ สคริปต์ก็รันได้แทบจะทันที จึงมีบางส่วนคล้าย Babashka ตอนนี้ยังอยู่ระหว่างเก็บรายละเอียด และยังไม่ใช่ v1.0
ในบล็อก เรื่อง “จัดการ runtime error โดยแก้โค้ดที่พังอยู่กับที่โดยไม่ต้อง restart” ควรจะเอาตัวอย่างนี้มาแสดง: https://www.youtube.com/watch?v=jBBS4FeY7XM
เมื่อรันการคำนวณที่ยาวและหนัก แล้วเกิด error ในขั้นตอนสุดท้าย แทนที่จะรันใหม่ตั้งแต่ต้น ก็เข้า interactive debugger ไปที่บรรทัดที่ผิด compile ฟังก์ชันที่แก้แล้ว จากนั้นกลับไปที่ debugger และ resume การทำงานจากจุดหนึ่งใน stack frame แล้วจะเห็นว่าโปรแกรมผ่านไปได้
M-รู้สึกว่าดีกว่าtap>ที่ Clojure มีให้มาก แม้จะใช้เครื่องมืออย่าง Portal ก็ยังมองว่าtap>ใกล้เคียงกับ pretty-print ที่แต่งหน้ามาแล้วมากกว่าด้านประสิทธิภาพ Common Lisp ก็สามารถระบุ static type ได้ และ SBCL จะสร้างโค้ดที่มีประสิทธิภาพมากเมื่อทำแบบนั้น การรัน
DISASSEMBLEกับโค้ดที่เขียนเองเพื่อดูว่าจริง ๆ แล้วปล่อยอะไรออกมาและ optimize นั้นมีประโยชน์ โดยเฉพาะแพ็กเกจอย่าง SB-SIMD และ Loopus ช่วยงานแอปพลิเคชันคำนวณเชิงตัวเลขได้มากจริง ๆ:quackเป็น true ก็ปฏิบัติกับมันเหมือนเป็ด” ที่ผสมกันได้ดี จึงเหมาะกับการออกแบบเกมแบบ entity-component-systemแต่ตลอด 1 ปีที่ผ่านมา คิดถึงวิธีพัฒนาของ Common Lisp และ ระบบ condition มากจริง ๆ ทั้งที่ก็ไม่ได้เป็นแฮ็กเกอร์ CL ที่เชี่ยวชาญขนาดนั้น แต่ก็ซึมซับได้เร็วมาก ถ้า CLOS กับชนิดข้อมูล primitive ของ CL เข้ากันได้ดีกว่าที่เห็นก็คงดี
ใน Cider ทำได้ง่ายแน่นอน และเครื่องมือหลัก ๆ ของ editor อื่นก็น่าจะทำได้ดีพอ ๆ กัน
DISASSEMBLEได้ในระดับหนึ่งมีโปรเจกต์เสริมอย่าง https://github.com/Bronsa/tools.decompiler และยังมี JitWatch ของ OpenJDK (https://github.com/AdoptOpenJDK/jitwatch) ด้วย JVM อื่น ๆ ก็มีเครื่องมือคล้ายกัน
แม้จะไม่ตรงไปตรงมาเท่า Lisp แต่ก็ทำได้อยู่ดี
การสร้างอะไรสักอย่างที่ลึกลับแต่ทำงานได้ดีอย่างน่าดึงดูดนั้นก็ถือว่าดี แต่ตอนนี้คงไม่มีใครอยากไปแตะมันแล้ว นอกจากคนที่บรรยายเงินเดือนตัวเองด้วยจำนวนเลขศูนย์ได้
https://www.digitalocean.com/community/tutorials/java-repl-j...
https://github.com/waf/CSharpRepl
https://pub.dev/packages/interactive
ยังไม่ต้องพูดถึง Ruby, Python, PHP, Lua แม้แต่ C++ ก็มี REPL แบบกระท่อนกระแท่น: https://github.com/root-project/cling
ทุกครั้งที่บอกว่า REPL ไม่ใช่จุดแตกต่างของฝั่ง Lisp อีกต่อไป ก็โดน downvote
https://github.com/kaveh808/kons-9
เพียงแต่การนิยามใหม่และเขียนทับแบบนี้สุดท้ายจะทำให้ namespace สกปรก และเมื่อถึงจุดหนึ่งบางอย่างก็พังจนต้องโหลดเซิร์ฟเวอร์ใหม่
มันมีข้อเสียเยอะเหมือนกัน แต่ก็ไม่ได้แย่ไปกว่าข้อเสียทางเทคนิคล้วน ๆ ของภาษาและสภาพแวดล้อมการพัฒนาอื่นแทบทั้งหมด Python กับ JS แพร่หลายกว่าและมีไลบรารีมากกว่า แต่รู้สึกว่าประสบการณ์พัฒนานั้นแย่กว่ามาก ผมใช้ C#, TS, Py, Hs และภาษาที่ลึกลับกว่านั้นมาเยอะหลายปี แต่ทุกครั้งที่ติดหล่มกับภาษาเหล่านั้นหรือหงุดหงิดอย่างหนักกับสิ่งที่แย่อย่างเห็นได้ชัด ก็จะกลับมาหา CL(SBCL + Emacs + Slime) แล้วรู้สึกโล่งใจ และมั่นใจว่าโลกนี้ยังมีสิ่งดี ๆ อยู่บ้าง
ตอนนี้เรากำลังระดมทุนให้ผลิตภัณฑ์ที่เราโง่พอจะ bootstrap ด้วย TypeScript แต่เวอร์ชันเปิดตัวจะทำใหม่ด้วย CL นั่นหมายความว่าในอีก 3–5 ปีก่อนเปิดตัว เราจะทำงานด้วย CL และภายใน CL ซึ่งก็ดีเพราะจะได้ทำงานสนุก ๆ อย่างการ implement DSL, การสร้างโค้ด, การทำงานกับ macro และการ implement ตัววิเคราะห์ static type
เวลาใช้ C# มีอะไรใน Lisp ที่คิดถึงบ้าง?
ความเห็นแบบสุดโต่งของผมคือ เหตุผลที่ functional programming ไม่ได้บูมในวงกว้าง เพราะแม้มันจะดีพอสำหรับการเขียนโปรแกรม แต่ซอฟต์แวร์ส่วนใหญ่ทุกวันนี้ไม่ใช่ “โปรแกรมที่รันจาก command line บนพีซี/เซิร์ฟเวอร์ของฉันแล้วทำงานไปจนจบ” แต่เป็น “โปรแกรมที่เริ่มทำงานแล้วตอบสนองต่ออินพุตของผู้ใช้จนกว่าผู้ใช้จะปิด” หรือ “โปรแกรมที่เริ่มทำงานแล้วตอบสนองต่อเครือข่ายหรือ I/O อัตโนมัติอื่น ๆ จนกว่าซอฟต์แวร์อื่นจะสั่งให้หยุด”
งานแบบนี้ทำในสไตล์ functional ล้วนได้ยากกว่ามาก หรืออย่างน้อยก็เป็นอย่างนั้นใน implementation แนว functional ที่มีรสนิยมชัดเจนซึ่งผมเคยใช้ เพราะมันไม่ใช่แค่การ evaluate expression บางอย่างอีกต่อไป แต่ต้อง initialize state, ตอบสนองต่อ I/O, อัปเดต state หรือทำ I/O เพิ่ม และอาจถึงขั้นใช้การทำงานแบบขนานเพื่อ monitoring, receiving, ทำ I/O เพิ่ม และอัปเดต state
แน่นอนว่าไม่ได้เป็นไปไม่ได้ที่จะทำสิ่งเหล่านี้ด้วย Lisp แต่จากประสบการณ์ที่ได้สัมผัส functional programming มาหลายภาคเรียนสมัยปริญญาตรี และใช้ฟีเจอร์ functional ใน C++/Scala อย่างมืออาชีพเพื่อแก้ปัญหาเหล่านี้ ผมรู้สึกว่าการทำให้ functional programming ทำงานได้ดีในแอปพลิเคชันแบบนี้ค่อนข้างยาก และความไม่เหมาะนี้ทำให้ผมลังเลที่จะลงลึกกับ functional programming มากขึ้น
นอกเหนือจาก Common Lisp แล้ว ผมมักต้องเขียนโค้ดที่สร้างโค้ดอีกทีอยู่บ่อย ๆ ในขอบเขตนี้ Lisp น่าจะโดดเด่น แต่ยังไม่มีโอกาสได้ลอง ตัวอย่างโปรเจกต์ล่าสุดที่ทำก่อนจะนึกถึง Common Lisp ก็มีการสร้างโค้ดสำหรับตรวจสอบ JSON Schema เฉพาะใน static site generator และงานสร้าง JSX จาก Markdown เพื่อใช้กับคอนเทนต์เนื้อเรื่องของเกมเขียนโปรแกรม
ยังไม่นับมาโคร C จำนวนนับไม่ถ้วนที่ใช้มาตลอด
และก็ไม่จำเป็นต้องทิ้งฟีเจอร์ดี ๆ ของสไตล์อื่น ๆ ไปทั้งหมด บางส่วนของการเขียนโปรแกรมเชิงฟังก์ชันก็พบได้บ่อยขึ้นเรื่อย ๆ ในภาษาทั่วไป เช่น Rust ก็ใช้แพตเทิร์นเชิงฟังก์ชันในหลายกรณี
และ Lisp ก็สามารถเขียนแบบเชิงวัตถุหรือเชิงคำสั่งได้ถ้าต้องการ มันไม่ใช่ Haskell
ผมมองว่า Lisp เป็นจุดเริ่มต้น และ Haskell คือของจริง แนะนำให้เรียนรู้สไตล์นั้น แม้จะไม่ได้เขียนโค้ดโปรดักชันจริงก็ตาม
(let ((pair (cons 1 nil)))(setf (cdr pair) pair)(list (first pair) (second pair) (third pair)));; => (1 1 1)ถ้าต้องการให้ “คอมไพล์ไปตลอดกาล” ก็เขียนด้วย ANSI C
ถ้าต้องการให้ “อยู่รอดไปตลอดกาล” ก็เขียนด้วย Python 2.7 แล้วทำให้มันเป็นแกนกลางของเทมเพลตอินฟราทั้งองค์กร ได้คะแนนพิเศษถ้าเป็นโมดูล Ansible แบบคัสตอม
สิ่งที่ผมหวังคือ Lisp จะช่วยลดหรือ abstraction สิ่งจุกจิกกับ boilerplate ให้น้อยที่สุด เพื่อให้ แสดงความคิดออกมาเป็นโค้ด ได้ง่ายขึ้น สำหรับผม Lisp expression ดูเหมือนเป็นวิธีที่เป็นธรรมชาติที่สุดในการแสดงความคิด
ผมสนใจการเขียนแอป GUI เป็นหลัก เลยอยากเรียน Clog หรือหา/พัฒนา wrapper ดี ๆ บน GUI toolkit สักตัว
(do-it (do-it first))(do-it first)ทำงานได้ดี แต่ถ้าการเรียก(do-it (do-it first))ล้มเหลว จะเกิดอะไรขึ้น?ถ้าได้ควบคุมที่จุดที่พังและแก้ definition ของ
do-itได้ก็คงดี แต่ถ้าแก้แล้ว ผลลัพธ์ของ(do-it first)ก็จะเปลี่ยนไปด้วยถ้าอย่างนั้น จุดที่เครื่องอยู่ตอนนี้ก็จะกลายเป็นจุดที่โค้ดปัจจุบันไม่สามารถไปถึงได้อีก
ผมไม่รู้จริง ๆ ว่ามันทำงานอย่างไรในกรณีที่การแก้ไขซึ่งทำให้ไปต่อได้ กลับเปลี่ยนสถานะที่เราได้รับการควบคุมมาเพื่อแก้นั้นเอง