ภาษา Python แบบโฮโมไอคอนิก (Homoiconic)
(aljamal.substack.com)- ความเป็นโฮโมไอคอนิก ของ Lisp คือคุณสมบัติที่ปฏิบัติต่อโค้ดและข้อมูลในรูปแบบเดียวกัน และเมื่อนำ “Lisp in Lisp” แบบคลาสสิกมาย้ายเป็น Python แนวคิดนี้ก็ปรากฏให้เห็นในไวยากรณ์ที่คุ้นเคยกว่า
- เดิมที Lisp มีทั้ง M-expression ซึ่งเป็นการแทนโค้ด และ S-expression ซึ่งเป็นการแทนข้อมูล ส่วน “Lisp in Lisp” คือการใช้ M-expression เพื่อ implement S-expression Lisp
- เวอร์ชัน Python แทน S-expression ด้วย ลิสต์ของ Python และย้าย M-expression ไปเป็นการเรียกฟังก์ชันกับคำสั่งเงื่อนไข จึงสร้าง interpreter ได้โดยไม่ต้องมี parser แยกต่างหาก
- interpreter ตัวแรกตั้งอยู่บน primitive operation สำหรับลิสต์ เช่น
atom,eq,car,cdr,cons,appendและเพื่อรองรับlambdaจึงเพิ่มassoc,pairlisและลิสต์สภาพแวดล้อมเข้ามา evalรับทั้ง expression และสภาพแวดล้อมเพื่อจัดการ variable binding และเชื่อมอาร์กิวเมนต์กับค่าตามแนวทาง dynamic scope ผ่านpairlisและassoc
การผสานโค้ดและข้อมูลที่ Lisp แสดงให้เห็น
- Lisp ทิ้งแนวคิดหลายอย่างที่ยังใช้ได้แม้ผ่านไปหลายสิบปี ผ่าน บทความ Lisp ของ John McCarthy ในช่วงต้นทศวรรษ 1960 และ คู่มือ Lisp 1.5
- แก่นสำคัญอย่างหนึ่งในนั้นคือ ความเป็นโฮโมไอคอนิก
- ในภาษาทั่วไป โค้ดมักถูกเข้าใจว่าเป็นลำดับของ operation ที่กระทำต่อข้อมูล
- Lisp ปฏิบัติต่อโค้ดและข้อมูลในรูปแบบเดียวกัน ทำให้เส้นแบ่งระหว่าง operator กับ operand พร่าเลือนลง
- Alan Kay เรียกโค้ด “Lisp in Lisp” ที่อยู่ท้ายหน้า 13 ของคู่มือ Lisp 1.5 ว่า “Maxwell's Equations of Software”
- มีการอ้างถึงแนวคิดว่า นี่เป็นการตระหนักรู้ครั้งใหญ่ เพราะโค้ดเพียงไม่กี่บรรทัดบรรจุโลกทั้งใบของการเขียนโปรแกรมไว้ได้
วิธีนำ “Lisp in Lisp” มาย้ายเป็น Python
- เป้าหมายคือการเขียนโค้ด “Lisp in Lisp” แบบคลาสสิกขึ้นใหม่ด้วย Python โดยรักษาจิตวิญญาณของโค้ดดั้งเดิมไว้ให้มากที่สุด
- Lisp มีรูปแบบไวยากรณ์สองแบบ
- M-expression: การแทนโค้ด ย่อมาจาก meta
- S-expression: การแทนข้อมูล ย่อมาจาก symbolic
- ทั้งสองรูปแบบเทียบเท่ากันในเชิงความหมาย
- โค้ด “Lisp in Lisp” เดิมเขียนด้วย M-expression เพื่อ implement S-expression Lisp
- ใน implementation แบบ Python จะใช้ ลิสต์ของ Python แทน Lisp S-expression
- Lisp ย่อมาจาก “List Processing” และทำงานโดยมีโครงสร้างข้อมูลเดียวคือ list เป็นศูนย์กลาง
- ลิสต์ของ Python ถูกใช้เป็นรูปแบบที่เหมาะสำหรับจำลอง Lisp S-expression
- M-expression ถูกแปลเป็นโครงสร้างโค้ดอย่างการเรียกฟังก์ชันและคำสั่งเงื่อนไขของ Python
- mapping นี้ทำให้สร้าง interpreter ได้โดยไม่ต้องจัดการสตริงหรือ implement parser แยกต่างหาก
interpreter ตัวแรกที่สร้างจาก primitive operation ของลิสต์
- การ implement Lisp ต้องมี ฟังก์ชันพื้นฐาน บางอย่างที่ภายนอกภาษาเป็นผู้จัดเตรียมให้
- primitive operation ของลิสต์ที่ใช้ใน implementation แบบ Python มีดังนี้
atom(x): ตรวจสอบว่าxเป็นลิสต์หรือไม่eq(x,y): ตรวจสอบว่าxกับyเหมือนกันหรือไม่car(x): องค์ประกอบแรกของลิสต์cdr(x): ส่วนที่เหลือของลิสต์cons(x,y): นำ atom ไปต่อเข้ากับลิสต์append(x,y): ต่อสองลิสต์เข้าด้วยกัน
- นอกจาก primitive operation แบบ recursive บางตัวแล้ว สามารถใช้ Llama3-70b บน Groq เพื่อสร้าง interpreter ที่รัน subset ของโค้ด “Lisp in Lisp” ได้อย่างรวดเร็ว
- ในตัวอย่าง ลิสต์ของ Python ทำงานเหมือน S-expression
- โค้ดทั้งหมดมีให้ดูที่ github gists
การขยายเพื่อรองรับ lambda และ recursion
- implementation แรกยังขาดฟีเจอร์สำคัญอย่าง
lambdalambdaเป็นวิธีหลักใน Lisp สำหรับนิยามและเรียก anonymous function- ใน Lisp หากไม่มี
lambdaก็ไม่สามารถ implement recursion ได้ - หากไม่มี recursion ก็ไม่ถึงเกณฑ์ขั้นต่ำของ Turing completeness ซึ่งหมายถึงความสามารถในการคำนวณทุกสิ่งที่คำนวณได้
- เพื่อใส่
lambdaจึงเพิ่มassoc(x,y)และpairlis(x,y)เข้ามาassoc(x,y)คือการ lookup key/value ที่ implement ด้วยลิสต์ โดยใช้ association listpairlis(x,y)จับคู่สองลิสต์เข้าด้วยกันเหมือนzip(x,y)ของ Python
- Lisp ดั้งเดิมต้องจัดการแม้แต่ linear scan ง่าย ๆ ด้วย recursion
- เพราะ Lisp ดั้งเดิมไม่มี loop
- ในคำแปลเป็น Python สามารถเขียน
assocและpairlisให้กระชับขึ้นด้วย list comprehension
- ในการจัดการ
CONDจะถอดevconของ Lisp ดั้งเดิมเป็น loop และในการจัดการLAMBDAก็ใช้แนวทางเดียวกันกับevlis
ลิสต์สภาพแวดล้อมและ dynamic scope
- ฟังก์ชัน
evalของ Lisp ดั้งเดิมรับอาร์กิวเมนต์สองตัว- อาร์กิวเมนต์แรกคือ S-expression ที่จะประเมินค่า
- อาร์กิวเมนต์ที่สองคือลิสต์สภาพแวดล้อมในรูปแบบรายการ key/value
- สภาพแวดล้อมรักษา variable binding ไว้ในการจัดการ
LAMBDA- หากฟังก์ชันมีตัวแปร
xและมีการใส่ข้อมูลเข้าไปpairlisจะจับคู่สัญลักษณ์xกับข้อมูล - ค่าที่จับคู่แล้วจะถูกบันทึกหรือเพิ่มลงในลิสต์สภาพแวดล้อม
- เมื่อต้องใช้
xassocจะค้นหาจากสภาพแวดล้อมแล้วนำกลับไปแทนใน expression
- หากฟังก์ชันมีตัวแปร
- วิธี binding นี้เรียกว่า dynamic scope
- implementation สุดท้ายคือรูปแบบที่ย้าย “Lisp in Lisp” ดั้งเดิมมาเป็น Python และในตัวอย่างสุดท้ายรวมถึงการรัน
lambdaด้วย
2 ความคิดเห็น
ความคิดเห็นบน Hacker News
ถ้าสนใจ Lisp สำหรับสคริปต์ที่เรียบง่ายและเร็วในสไตล์ Python ก็น่าดูภาษาแนว Clojure อย่าง Hy และ Janet
มีให้ใช้เป็นไบนารีที่เริ่มทำงานได้เร็ว และรวมไลบรารีสำหรับงานประจำวันไว้ด้วย
ฝั่ง CL↔Python ก็น่าดู https://github.com/CodyReichert/awesome-cl?tab=readme-ov-fil... เช่นกัน
ผมเป็น ผู้ดูแล Hy ถ้าเห็นความต่างด้านประสิทธิภาพที่มีนัยสำคัญ นั่นคือบั๊ก
M-expression เลี่ยงกับดักนี้ได้ เลยสงสัยว่าเคยมีภาษาจริงที่ไม่ต้องใช้วงเล็บแบบนี้ แต่ยังคงความเป็น homoiconicity และความสง่างามเชิงแนวคิดของ Lisp ไว้หรือไม่
นี่แหละ สิ่งที่คิดอยู่พอดี คำอธิบายก็ตรงไปตรงมามาก: “Program in YAML”
แทบไม่ต้องพูดอะไรเพิ่มแล้ว
เดี๋ยวนะ นี่ก็แค่ Lisp ที่เขียนด้วย Python ไม่ใช่เหรอ?
ดูไม่เหมือน Python แบบ homoiconic ตามที่ชื่อสื่อไว้ ผมพลาดอะไรไปหรือเปล่า?
ถ้าจะทำแบบนั้น ไวยากรณ์ของ Python คงต้องประกอบจากทูเพิล ลิสต์ และดิกชันนารี แล้วอินเทอร์พรีเตอร์ต้องประเมินสิ่งเหล่านั้นโดยตรง
อีกภาษาฟังก์ชันนัลที่สามารถอิมพลีเมนต์ได้อย่างกระชับด้วย Python คือ Binary Lambda Calculus
โค้ดส่วนใหญ่จัดการกับโมเดลอินพุต/เอาต์พุตบริสุทธิ์ของ BLC และสำหรับการค้นหาตัวแปร จะใช้ de Bruijn index เพื่ออินเด็กซ์อาร์เรย์สภาพแวดล้อมแทนการใช้ association list
ในหน้าเดียวกันยังมีอิมพลีเมนต์ภาษาอื่นอีก 9 ภาษา และ self-interpreter ของ BLC กระชับที่สุดที่ 232 บิต (29 ไบต์) รวมทั้งพาร์เซอร์และโทเคนไนเซอร์แล้ว
[1] https://rosettacode.org/wiki/Universal_Lambda_Machine#Python
วิชาพื้นฐานการเขียนโปรแกรมของ MIT ให้ผู้เรียนทุกคนเขียน อินเทอร์พรีเตอร์ Lisp ด้วย Python
เป็นร่องรอยจากสมัยก่อนที่วิชานั้นสอนด้วย Lisp จริง ๆ
https://py.mit.edu/spring24
ผมเคยลองทำอะไรคล้าย ๆ กันด้วยลิสต์ของ JS: https://github.com/andrelaszlo/js-lisp
เป็นเรื่องทั้งตลกและประชดที่ภาษา Store definition file ในยุคใหม่ ๆ ค้นพบฟีเจอร์น่าทึ่งของ Lisp อีกครั้งหลังผ่านไปหลายสิบปี
ไม่กี่วันก่อน โปรแกรม Python ที่เรียก API โฮมเซิร์ฟเวอร์ของผมมา 9 ชั่วโมงหยุดทำงาน เลยโมโหมาก เป็นการเรียก API คล้าย ๆ กัน และกำลังเรียก LLM ด้วยเทมเพลตพรอมป์ต์ที่กำหนดไว้ล่วงหน้ากับข้อจำกัดทางไวยากรณ์
ก่อนจะรันลูปที่เหลือแยกต่างหาก ผมอยากบันทึกสถานะของโปรแกรมแล้วปิด แต่หาวิธีแก้โค้ด Python ที่กำลังรันอยู่หรือส่องดูตัวแปรไม่ได้ สุดท้ายเลยเสีย งาน 9 ชั่วโมง ไป
ไม่กี่วันต่อมาผมเห็น https://malisper.me/debugging-lisp-part-1-recompilation/ แล้วก็ทึ่งที่ Common Lisp มีฟีเจอร์แบบนี้ฝังอยู่ในภาษามาหลายสิบปีแล้ว ฟีเจอร์อื่น ๆ อย่างระบบมาโครที่ทรงพลังที่สุดก็เช่นกัน
แต่นั่นก็เป็นปัญหาด้วย คือ พลังในการสื่อความหมายสูงเกินไป
Lisp ทำให้ผมนึกถึงทัศนศิลป์สื่อผสมอยู่เสมอ อิสระในการแสดงออกดูดี แต่สุดท้ายผลงานมักออกมาด้อยกว่างานศิลป์แบบสื่อเดี่ยวที่ดั้งเดิมกว่าโดยรวม จึงเห็นได้ว่าข้อจำกัดของสื่อก็สำคัญพอ ๆ กับพลังในการสื่อความหมาย
หรือมี https://github.com/malor/cpython-lldb ด้วย
วิธีเพิ่มเติมดูได้ที่ https://github.com/albertz/pydbattach/
การทำแบบนี้เป็น Python แบบ homoiconic อย่างไร?
มี Lisp ที่มี ระบบชนิดข้อมูล ซึ่งช่วยควบคุมแนวโน้มที่ Lisp จะอ่านยากขึ้นเมื่อโปรแกรมใหญ่ขึ้นได้บ้างไหม?
ถึงไม่ใช่ระบบชนิดข้อมูล ผมก็สงสัยว่ามีองค์ประกอบอื่นที่ช่วยจัดการคุณสมบัติแบบนั้นได้หรือเปล่า
เมตาโปรแกรมมิงเจ๋งมากจริง ๆ แต่บางครั้งมันอ่านเหมือน Haskell ที่เป็นนามธรรมชวนงงที่สุดเท่าที่เคยเห็น แถมไม่มีแม้แต่ type signature ให้เป็นเข็มทิศ
ผมมองว่าระบบชนิดข้อมูลกับลินเตอร์เป็นเครื่องมือที่ดีที่สุดในการทำให้โค้ดเชื่องโดยอัตโนมัติ แต่ยังนึกไม่ออกว่าจะยับยั้งแนวโน้มของโปรเจกต์ Lisp ได้อย่างไร โดยไม่ไปจำกัดเหตุผลหลักที่เลือกใช้ Lisp เอง
SBCL ซึ่งแทบจะใกล้เคียงมาตรฐานที่สุด ก็มีการตรวจชนิดตอนคอมไพล์ที่ค่อนข้างดี
เช่น ถ้าเขียน
(declare (type String a b))แล้วตามด้วย(+ a b)มันจะเตือนว่าชนิดอนุมานของAคือSTRINGแต่ควรเป็นNUMBERเหตุผลใหญ่ที่สุดที่ผมย้ายจาก Scheme มา CL ก็คือการตรวจชนิดนี้ และหลังจากนั้นก็อยู่ต่อเพราะฟีเจอร์เล็ก ๆ ดี ๆ อย่าง restarts กับ continuable asserts
ถ้ามี Python ที่เน้น expression คงดีกว่า Python ตอนนี้มาก
บทความนี้ไม่ได้เป็นอย่างที่ชื่อชวนให้คิด แต่ในฐานะคำอธิบาย Lisp ถือว่าดี
ถึงอย่างนั้นก็ไม่เป็นไร แต่ “การวาง expression ไว้เป็นไวยากรณ์ชั้นสอง” เป็นการเลือกที่ค่อนข้างพื้นฐานและตั้งใจมาก อีกทั้งยังเป็นการออกแบบให้ยากสำหรับนักพัฒนาที่อยากเขียนซุปวงเล็บด้วย
ดังนั้น
lambda:และ:=จึงถูกทำให้เทอะทะโดยเจตนาเพื่อกดไม่ให้ใช้ และก็เขียนฟังก์ชันนิรนามไม่ได้ด้วยPython เป็นภาษาที่บอกว่าถ้าอยากทำอะไรฉลาด ๆ ให้ทำด้วย iterator ไม่ใช่วัตถุที่เรียกได้
อยากให้ทุกอย่างมี ความไร้ไวยากรณ์แบบ Lisp
สำหรับผม แบบนั้นดีกว่า และโชคดีที่ใน CL ทำแบบนั้นได้ง่าย
ภาษาถิ่นของ Lisp ที่ฝังอยู่ใน Python
https://hylang.org/