ความเห็นของ Hofstadter ว่าด้วย Lisp (1983)
(gist.github.com/jackrusher)- บทความของ Douglas Hofstadter ในปี 1983 แนะนำ Lisp ซึ่งถูกใช้อย่างแพร่หลายในงานวิจัย AI ว่าเป็นภาษาที่สง่างามและยืดหยุ่น ซึ่งสร้างอยู่บนแกนคณิตศาสตร์ขนาดเล็ก
- ความเป็นภาษาที่โต้ตอบได้ของ Lisp ปรากฏชัดใน read-eval-print loop ซึ่งผู้ใช้ป้อนนิพจน์เข้าไป จากนั้นอินเทอร์พรีเตอร์จะอ่าน ประเมินผล แสดงผล แล้วรออินพุตถัดไป
- อ็อบเจ็กต์ของ Lisp ประกอบขึ้นโดยมี อะตอม(atom) และ ลิสต์(list) เป็นแกนหลัก โดย
nilมีสถานะพิเศษคือเป็นทั้งลิสต์ว่างและอะตอม quote,eval,car,cdr,cons,setq,set,lambda,def,condทำให้สามารถจัดการโค้ดและข้อมูลในโครงสร้างลิสต์แบบเดียวกันได้ จึงค่อย ๆ สร้างฟังก์ชันใหม่ขึ้นมาได้- ตัวอย่างต่าง ๆ เชื่อมโยงการประกอบฟังก์ชัน นิพจน์เงื่อนไข side effect กับสไตล์แบบ applicative และการคิดเชิงเรียกซ้ำเข้าด้วยกัน โดยปิดท้ายด้วยฟังก์ชัน
powerและอุปมาเรื่อง “porpuquine” ที่ชี้ไปสู่การเรียกซ้ำแบบ Lisp
ตำแหน่งของ Lisp ในงานวิจัย AI
- ในปี 1983 AI เป็นสาขาวิจัยที่พยายามทำให้คอมพิวเตอร์แสดงพฤติกรรมอย่างความยืดหยุ่น สามัญสำนึก ความเข้าใจลึกซึ้ง ความคิดสร้างสรรค์ การตระหนักรู้ตนเอง และอารมณ์ขัน
- ในสหรัฐฯ มีผู้ที่ทำงานเกี่ยวกับ AI โดยตรงราว 2,000 คน และในต่างประเทศก็มีนักวิจัยจำนวนใกล้เคียงกัน
- นักวิจัย AI มีความเห็นแตกต่างกันมากว่าเส้นทางที่ดีที่สุดสู่ AI คืออะไร แต่ในเรื่องการเลือกภาษาโปรแกรมนั้นแทบทุกคนใช้ Lisp ร่วมกัน
- ชื่อ Lisp มาจาก “list processing” และไม่ใช่คำย่อเต็มรูปแบบ
- เสน่ห์ของ Lisp อยู่ที่การเป็นภาษาที่ “คมชัด” และ “สง่างาม”
- ขณะที่หลายภาษายัดฟีเจอร์แบบตามอำเภอใจจำนวนมากเข้าไว้ด้วยกัน บางภาษาอย่าง Lisp และ Algol กลับถูกจัดวางรอบแกนธรรมชาติที่คล้ายสาขาหนึ่งของคณิตศาสตร์
- แกนของ Lisp มี “crystalline purity” และความบริสุทธิ์นี้ไม่ได้มีคุณค่าด้านสุนทรียะเท่านั้น แต่ยังช่วยเพิ่มความยืดหยุ่นด้วย
จากตรรกศาสตร์คณิตศาสตร์สู่ภาษาประมวลผลลิสต์
- รากที่ลึกของ Lisp อยู่ใน ตรรกศาสตร์คณิตศาสตร์
- แนวคิดที่ Thoralf Skolem, Kurt Godel และ Alonzo Church ทิ้งไว้ให้แก่วงการตรรกศาสตร์ในช่วงทศวรรษ 1920–1930 ได้ถูกนำมารวมไว้ใน Lisp ในเวลาต่อมา
- การเขียนโปรแกรมคอมพิวเตอร์อย่างจริงจังเริ่มขึ้นในทศวรรษ 1940 และภาษาระดับสูงอย่าง Lisp ก็ปรากฏขึ้นในทศวรรษ 1950
- ภาษาประมวลผลลิสต์ภาษาแรกไม่ใช่ Lisp แต่คือ IPL (Information Processing Language)
- IPL ถูกพัฒนาในช่วงกลางทศวรรษ 1950 โดย Herbert Simon, Allen Newell และ J. C. Shaw
- John McCarthy สร้าง Lisp ขึ้นในช่วงปี 1956–1958 โดยต่อยอดจากแนวคิดที่มีอยู่เดิมให้เป็นภาษาประมวลผลลิสต์เชิงพีชคณิต
- Lisp แพร่กระจายอย่างรวดเร็วในกลุ่มนักวิจัยรุ่นใหม่รอบ ๆ MIT Artificial Intelligence Project
- มันถูกนำไปติดตั้งบน IBM 704 และแพร่ต่อไปยังกลุ่ม AI อื่น ๆ
- แม้จะเกิดภาษาถิ่นย่อยหลายแบบขึ้นมา แต่ทุกแบบก็ยังใช้แกนกลางอันสง่างามร่วมกัน
Lisp แบบอินเทอร์แอ็กทีฟและ REPL
- Lisp ถูกนำเสนอว่าเป็นภาษาแบบ โต้ตอบได้ ต่างจากภาษาระดับสูงอื่น ๆ จำนวนมาก
- ผู้ใช้รัน Lisp บนเทอร์มินัล จากนั้นมองเห็นพรอมป์ต์และป้อนนิพจน์เข้าไป
- ตัวอย่าง: ป้อน
(plus 2 2)แล้วจะได้4แสดงออกมา พร้อมพรอมป์ต์ใหม่
- ตัวอย่าง: ป้อน
- กลไกหลักของอินเทอร์พรีเตอร์ Lisp คือ read-eval-print loop
- อ่านนิพจน์
- ประเมินนิพจน์
- แสดงค่าที่เหมาะสม
- แจ้งว่าพร้อมอ่านนิพจน์ถัดไปแล้ว
- ด้วยโครงสร้างนี้ ผู้ใช้จึงสามารถป้อน “ความปรารถนา” ได้ทีละอย่างและเห็นผลลัพธ์ทันที
- ในภาษาที่ไม่โต้ตอบจำนวนมาก ต้องเขียนโปรแกรมทั้งก้อนให้เสร็จก่อนแล้วค่อยรัน ทำให้หลายขั้นตอนต้องประกบกันอย่างสมบูรณ์ และการแก้ข้อผิดพลาดก็เป็นกระบวนการทางอ้อมและมีต้นทุนสูง
- Lisp เปิดทางให้ การพัฒนาและดีบักแบบค่อยเป็นค่อยไป ด้วยวิธีรันนิพจน์ทีละตัว
Polish notation และวงเล็บ
- นิพจน์เลขคณิตของ Lisp ใช้ Polish notation ซึ่งวางตัวดำเนินการไว้หน้าตัวถูกดำเนินการ
- ตัวอย่าง:
(times (plus 6 3) (difference 6 3))จะประเมินค่าได้ 27
- ตัวอย่าง:
- สัญกรณ์นี้ถูกคิดค้นขึ้นโดย Jan Lukasiewicz นักตรรกศาสตร์ชาวโปแลนด์ก่อนยุคคอมพิวเตอร์
- นิพจน์ Lisp มักเต็มไปด้วยวงเล็บ
- บ่อยครั้งนิพจน์ยาว ๆ จะจบลงด้วยวงเล็บปิดหลายตัวติดกัน
- ตอนแรกอาจดูน่าหนักใจ แต่เมื่อคุ้นเคยแล้วจะมองเห็นโครงสร้างเชิงตรรกะได้อย่างเป็นธรรมชาติ
- ตัวอย่างต่าง ๆ ถูกนำเสนอในรูปแบบ pretty printing ที่ใช้การเยื้องเพื่อเผยโครงสร้าง
อะตอม ลิสต์ และ nil
- หัวใจของ Lisp คือโครงสร้างที่สามารถจัดการได้ และทุกโปรแกรมก็ทำงานด้วยการสร้าง แก้ไข และทำลายโครงสร้างเหล่านี้
- โครงสร้างมีอยู่สองชนิด
- อะตอม(atom): อ็อบเจ็กต์พื้นฐานที่ไม่สามารถแยกย่อยต่อไปได้
- ลิสต์(list): โครงสร้างที่มีหลายองค์ประกอบเรียงตามลำดับเฉพาะ
- อ็อบเจ็กต์ทุกตัวใน Lisp เป็นอะตอมหรือลิสต์ ยกเว้นเพียงกรณีเดียวคือ nil
nilเป็นทั้งอะตอมและลิสต์nilแทนลิสต์ว่าง()และnilมีความหมายเหมือนกัน แต่nilถูกใช้บ่อยกว่า
- องค์ประกอบของลิสต์อาจเป็นอะตอมหรือลิสต์ก็ได้
- ตัวอย่าง:
(zonk blee strill (croak flonk))เป็นลิสต์ที่มี 4 องค์ประกอบ โดยองค์ประกอบสุดท้ายเป็นลิสต์ย่อยที่มี 2 องค์ประกอบ
- ตัวอย่าง:
- ประโยคของ Lisp เองก็เป็นลิสต์เช่นกัน
(plus 2 2)ก็เป็นลิสต์- อินเทอร์พรีเตอร์ Lisp สามารถจัดการลิสต์และอะตอมเพื่อสร้างและประเมิน “คำสั่ง” ใหม่ได้
ค่า, quote, eval
- อะตอมสามารถมีค่าได้
- อะตอมตัวเลขมีตัวมันเองเป็นค่าถาวร
- ค่าของ
nilคือnil tก็เป็นอะตอมพิเศษที่มีตัวมันเองเป็นค่าถาวรเช่นกัน
setqใช้กำหนดค่าให้แก่อะตอม(setq pie 4)ทำให้ค่าของpieเป็น 4(setq pie (plus 2 2))ก็ทำให้ค่าของpieเป็น 4 เช่นกัน
- ค่าของอะตอมไม่ได้จำกัดแค่ตัวเลข แต่อาจเป็นอ็อบเจ็กต์ Lisp ใด ๆ ก็ได้
- จะเป็นอะตอมหรือลิสต์ก็ได้ทั้งนั้น
- quote ใช้ป้องกันไม่ให้เกิดการประเมินผล และทำให้ลิสต์หรืออะตอมถูกมองเป็นข้อมูลตามตัวของมันเอง
(setq pie (plus 2 2))จะประเมินนิพจน์ด้านในแล้วเก็บค่า 4 ไว้ในpie(setq pi '(plus 2 2))จะเก็บลิสต์(plus 2 2)เองไว้ในpi
evalใช้ประเมินนิพจน์ที่ถูกเก็บเป็นค่าไว้อีกครั้ง- ถ้าค่าของ
piคือ(plus 2 2)แล้ว(eval pi)จะคืนค่า 4 - โดยปกติอินเทอร์พรีเตอร์จะประเมินเพียงหนึ่งชั้น แต่เมื่อใช้
evalก็สามารถร้องขอการประเมินเพิ่มเติมได้
- ถ้าค่าของ
car, cdr, cons
- ลิสต์ทุกตัวที่ไม่ใช่
nilจะต้องมีอย่างน้อยหนึ่งองค์ประกอบ และองค์ประกอบแรกนั้นเรียกว่า carcarของ(eval pi)คือevalcarของ(plus 2 2)คือplus
carไม่จำเป็นต้องเป็นชื่อฟังก์ชันเสมอไปcarของ((1)(2 2) (3 3 3))คือ(1)
- เมื่อตัด
carออกไป ลิสต์ที่เหลือเรียกว่า cdrcdrของ(a b c d)คือ(b c d)- จากนั้นจะลดลงต่อเป็น
(c d),(d), และnil
nilไม่มีทั้งcarและcdrและหากพยายามเรียกcarหรือcdrกับมันก็ควรเกิดข้อผิดพลาดconsใช้เพิ่มcarใหม่ไว้หน้าลิสต์เดิมเพื่อสร้างลิสต์ใหม่- ถ้า
xคือ(cake cookie)แล้ว(cons 'pie x)จะสร้าง(pie cake cookie) - โดยที่
xเดิมจะไม่ถูกเปลี่ยนแปลง
- ถ้า
- การมีหรือไม่มี quote ทำให้ผลลัพธ์ต่างกัน
- ถ้าค่าของ
pieคือ 4 แล้ว(cons pie x)จะคืนค่า(4 cake cookie) (cons 'pie x)จะนำชื่ออะตอมpieเองไปวางไว้ข้างหน้า
- ถ้าค่าของ
ความต่างระหว่าง setq กับ set
- อินเทอร์พรีเตอร์ Lisp ไม่ได้ประเมินอาร์กิวเมนต์ทุกตัวเสมอไป และ
setqเป็นข้อยกเว้นตัวอย่างสำคัญ - อาร์กิวเมนต์ตัวแรกของ
setqจะไม่ถูกประเมิน แต่ถูกมองตรง ๆ ว่าเป็นชื่อตัวแปร- ตัว
qในsetqหมายถึง quote และสื่อว่าอาร์กิวเมนต์ตัวแรกถูกปฏิบัติเสมือนถูก quote ไว้
- ตัว
setคล้ายsetqแต่จะประเมินอาร์กิวเมนต์ตัวแรกด้วย- ถ้าค่าของ
xคือkแล้ว(set x 7)จะไม่ได้เปลี่ยนxแต่จะเปลี่ยนค่าของkให้เป็น 7 แทน
- ถ้าค่าของ
- มีการยกตัวอย่างลำดับต่อไปนี้
(setq a 'b)(setq b 'c)(setq c 'a)(set a c)(set c b)
- หลังจากรันตามนี้แล้ว ค่าของ
a,b,cจะกลายเป็นaทั้งหมด setไม่ได้ถูกใช้บ่อยนัก และความสับสนแบบนี้ก็ไม่ได้เกิดขึ้นบ่อยเช่นกัน
การนิยามฟังก์ชันและ lambda
- พลังสำคัญของการเขียนโปรแกรมคือความสามารถในการนิยามปฏิบัติการประกอบใหม่จากปฏิบัติการที่มีอยู่ แล้วทำซ้ำกระบวนการนั้นเพื่อสะสมคลังปฏิบัติการที่ซับซ้อนขึ้นเรื่อย ๆ
- ใน Lisp ฟังก์ชันใหม่ถูกนิยามจากฟังก์ชันที่รู้จักอยู่แล้ว
racเป็นตัวอย่างฟังก์ชันที่คืนค่าสมาชิกตัวสุดท้ายของลิสต์- ถ้าต้องการสมาชิกตัวสุดท้าย ก็สามารถกลับลิสต์แล้วใช้
car - นิยามคือ
(def rac (lambda (lyst) (car (reverse lyst))))
- ถ้าต้องการสมาชิกตัวสุดท้าย ก็สามารถกลับลิสต์แล้วใช้
(lyst)หลังlambdaแสดงพารามิเตอร์ของฟังก์ชัน หรือก็คือตัวแปรหลอก- เมื่อนิยามด้วย
defแล้วracก็กลายเป็นฟังก์ชันที่ใช้ได้เหมือนcar(rac '(your brains))จะคืนค่าbrains
- ฟังก์ชัน
readers-digest-condensed-versionสร้างลิสต์สั้นจากลิสต์ยาว โดยเก็บไว้เฉพาะสมาชิกตัวแรกและตัวสุดท้าย- ถ้ามอง Finnegans Wake ทั้งเล่มของ James Joyce เป็นลิสต์ของคำ ก็จะได้ตัวอย่างผลลัพธ์เป็น
(riverrun the)
- ถ้ามอง Finnegans Wake ทั้งเล่มของ James Joyce เป็นลิสต์ของคำ ก็จะได้ตัวอย่างผลลัพธ์เป็น
- ปฏิบัติการย้อนกลับชื่อ
rejoyceเป็นฟังก์ชันในจินตนาการที่สร้างนวนิยายจากคำต้นและคำท้ายสองคำ และถูกทิ้งไว้ให้ผู้อ่านลองเขียนเอง
การคืนค่า, side effect, และสไตล์แบบ applicative
- บางคนเชื่อว่าใน Lisp และภาษาที่เกี่ยวข้อง มีเป้าหมายสองข้อที่ทั้งพึงปรารถนาและเป็นไปได้
- ทุกประโยคต้องคืนค่า
- ผลของประโยคควรเกิดขึ้นผ่านค่าที่คืนกลับมาเท่านั้น
- ภาษาถิ่นของ Lisp ที่กล่าวถึงในบทความนี้ทำได้ตามเงื่อนไขข้อแรก แต่ไม่จำเป็นต้องตรงตามข้อที่สองเสมอไป
(reverse x)จะไม่เปลี่ยนxเอง แต่จะสร้างลิสต์ใหม่ที่เหมือนเดิมทุกอย่างยกเว้นลำดับที่กลับด้าน แล้วคืนค่านั้นออกมา- คล้ายกับที่
(plus 2 2)ไม่ได้เปลี่ยนค่าของ 2
- คล้ายกับที่
consก็เช่นกัน มันคืนลิสต์ใหม่โดยไม่แก้ไขลิสต์เดิม- ตรงกันข้าม
setqเป็นตัวอย่างของคำสั่งที่ทิ้ง side effect เอาไว้ด้วยการเปลี่ยน binding ของตัวแปร- side effect อาจรวมถึงการเปลี่ยน binding ของตัวแปรและการรับส่งข้อมูลเข้าออก
- ผู้สนับสนุนการเขียนโปรแกรมแบบ applicative ชอบแนวทางที่ฟังก์ชันคำนวณค่าและส่งต่อค่าโดยไม่มี side effect เท่านั้น
- ในสไตล์นี้ binding ที่ยอมรับได้คือ lambda binding ชั่วคราวที่เกิดขึ้นระหว่างการเรียกฟังก์ชัน
- เมื่อการคำนวณของฟังก์ชันสิ้นสุดลง binding ของตัวแปรหลอกก็หายไป
- ผู้เขียนมองว่าสไตล์ applicative มีความสง่างาม แต่เห็นว่าไม่ค่อยเหมาะในทางปฏิบัติสำหรับการสร้างโปรแกรมสไตล์ AI ขนาดใหญ่
- ในการเขียนโปรแกรมแบบ applicative อย่างเคร่งครัด แม้แต่
defก็อาจถือว่าเป็นกรณีสุดโต่งที่ยอมรับไม่ได้ เพราะมันเก็บนิยามฟังก์ชันถาวรไว้ในหน่วยความจำ
นิพจน์เงื่อนไข cond และการตัดสินใจ
- หาก Lisp จะทำสิ่งที่น่าสนใจมากขึ้น มันต้องสามารถตัดสินใจตามสิ่งที่เกิดขึ้นระหว่างทางได้ และตรงนี้เองที่ต้องใช้ นิพจน์เงื่อนไข
- ตัวอย่าง:
(cond ((eq x 1) 'land) ((eq x 2) 'sea))- ถ้า
xเป็น 1 จะได้land - ถ้า
xเป็น 2 จะได้sea - นอกเหนือจากนั้นจะคืนค่า
nil
- ถ้า
eqเป็นฟังก์ชัน Lisp ที่คืนค่าtถ้าค่าของอาร์กิวเมนต์ทั้งสองเท่ากัน และคืนnilถ้าไม่เท่ากัน- ประโยค
condเริ่มต้นด้วยชื่อฟังก์ชันcondและมีหลาย clause- แต่ละ clause เป็นลิสต์สองสมาชิกที่ประกอบด้วยเงื่อนไขและผลลัพธ์
- มันจะตรวจเงื่อนไขไปตามลำดับ เมื่อพบเงื่อนไขแรกที่คืนค่าซึ่งไม่ใช่
nilก็จะประเมินผลลัพธ์ของ clause นั้นและคืนเป็นค่าของcondทั้งหมด - clause หลังจากนั้นจะไม่ถูกตรวจต่อ
- ถ้าเติม clause สุดท้ายที่ใช้
tเป็นเงื่อนไขแบบ catch-all ก็สามารถคืนค่าปริยายแทนnilได้- ตัวอย่าง
condที่ยกมาจะแสดงการคืนค่าairหากทั้งเงื่อนไขlandและseaล้มเหลว
- ตัวอย่าง
กำลังยกและโครงสร้างแบบเรียกซ้ำ
- ช่วงท้ายบทความมีการนิยามฟังก์ชันที่มีรูปแบบชัดเจน
squareคือk * kcubeคือk * square(k)4th-powerคือk * cube(k)- และดำเนินต่อไปในลักษณะเดียวกัน
- จึงมีคำถามว่าหรือจะนิยามฟังก์ชันสองพารามิเตอร์ชื่อ
powerที่ครอบคลุมรูปแบบทั้งหมดนี้ได้ในคราวเดียว(power 9 3)ควรได้ 729(power 7 4)ควรคืนค่า 2,401
- เครื่องมือที่จำเป็นทั้งหมดถูกอธิบายไว้แล้วในเนื้อหา และสิ่งที่ต้องใช้ต่อคือไหวพริบของผู้อ่าน
- เรื่อง porpuquine แห่ง Glazunkia ตอนท้ายใช้เป็นอุปมาว่าด้วยโครงสร้างแบบเรียกซ้ำ
- หนามของ porpuquine แต่ละอันก็คือ porpuquine ที่เล็กกว่า
- ใน Outer Glazunkia จะมีหนามอยู่ 9 อันเสมอ และใน Inner Glazunkia จะมีอยู่ 7 อันเสมอ
- porpuquine ขนาด 0 นิ้วจะไม่มีหนาม จึงทำให้การถอยกลับแบบไม่สิ้นสุดหยุดลง
- “buying power” หรือ “power” ของสัตว์ชนิดนี้เชื่อมโยงกับจำนวนจมูกของ porpuquine ขนาด 0 นิ้วตัวเล็ก ๆ ที่อยู่ภายใน และปิดท้ายเป็นอุปมาที่โยงกลับไปยังโจทย์
powerก่อนหน้า
บริบทของการนำต้นฉบับมาตีพิมพ์ซ้ำ
- ส่วนต้นของ Gist ระบุว่านี่คือบทแนะนำ Lisp ของ Douglas Hofstadter ที่ผู้เรียบเรียงได้พบในฉบับเก่าของ Scientific American ช่วงกลางทศวรรษ 1980
- ตัวอย่างต่าง ๆ ยังสามารถรันได้ใน Emacs หากติดตั้ง alias เพิ่มอีกเล็กน้อย
plus→+quotient→/times→*difference→-
- ตอนท้ายระบุว่าหากชอบคำแนะนำสู่ Lisp ในแบบเฉพาะตัวของ Hofstadter ก็สามารถอ่านบทความลักษณะคล้ายกันได้อีกในหนังสือ Metamagical Themas ของเขา
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ตอนแรกสับสนกับชื่อฟังก์ชัน
"oval"และ"snot"ในตัวอย่าง แต่ไม่กี่วินาทีต่อมาก็รู้ว่ามันเป็นการพิมพ์ผิดของ"eval"และ"snoc"ตามลำดับโค้ดเดิมควรอ่านเป็น
(cond ((eq (eval pi) pie) (eval (snoc pie pi))) (t (eval (snoc (rac pi) pi))))ลองไปดูบทความ Scientific American ต้นฉบับจาก https://www.jstor.org/stable/24968822 แล้วตรวจสอบดู พบว่า
oval/snotเป็นการพิมพ์ผิดของeval/snocจริงๆ"oval"กับ"snot"เป็นการพิมพ์ผิดของ"eval"กับ"snoc"ถูกแล้วและ
snocดูเหมือนจะเป็นconsที่กลับด้าน ส่วนracดูเหมือนจะเป็นcarที่กลับด้านบทความนี้แสดงให้เห็นอีกครั้งว่า สาเหตุสำคัญที่ Lisp ไม่ค่อยได้รับความนิยมคือ วิธีอธิบาย Lisp
บทความ Lisp แบบนี้ไม่ได้บอกอะไรที่ผมในวัยเด็กเอาไปใช้ได้จริงเลย และก็ไม่ได้แสดงให้เห็นว่างาน X บางอย่างจะง่ายขึ้นแค่ไหนเมื่อเทียบกับ Assembly/C/Pascal ฯลฯ
ถ้าเป็นผมตอนนี้คงจะสาธิตว่า “ตัวตรวจสะกดที่ใช้เวลา 7 เดือนใน Assembly น่ะเหรอ? บนไมโครคอมพิวเตอร์ที่ใช้ bank-switched memory ถ้าใช้ Lisp แทบจะเป็นเรื่องเล็กน้อย และยังทำ garbage collection ให้ deterministic ได้แม้บน CPU ห่วยๆ”
บทความและตำรา Lisp จำนวนมากเอาแต่พูดซ้ำเรื่องลิสต์ รีเคอร์ชัน และ AI แต่ไม่แสดงวิธีทำงานที่มีประโยชน์ ทำให้รู้สึกเสียดายเวลาราวกับเขียนโปรแกรมด้วยแหนบ เม็ดข้าว และกาว
นิตยสาร Byte ฉบับแรกที่ผมเห็นตอนเด็กมีโค้ด Lisp สำหรับ symbolic differentiation และ algebraic simplification ถึงจะตามอ่านยาก แต่ก็รู้สึกว่ามันมีอะไรดึงดูดบางอย่าง
มันไม่น่าจะง่ายกว่าถ้าเขียนด้วย Basic และภายหลังถึงได้รู้ว่าโค้ดนั้นก็ไม่ได้ยอดเยี่ยมมากนัก แต่กว่าจะได้หลงใหล Lisp จริงๆ ก็หลังจากได้เจอ XLisp บน PC ช่วงปลายยุค 80 และ SICP
ผมชอบงานเขียนของ Hofstadter มาก เขาจับความรู้สึกตอนค้นพบ Lisp ได้ดีจริงๆ
ตอนยุค 80 ผมเป็นเด็กที่กำลังเรียนโปรแกรมมิง และช่วงมัธยมปลายกับต้นมหาวิทยาลัยก็เคยลอง BASIC, Fortran, Pascal, COBOL มาบ้างแล้ว แม้จะต่างกัน แต่โดยพื้นฐานก็มีจุดร่วมกัน
แต่คอร์สวิทยาการคอมพิวเตอร์วิชาแรกที่ UC Berkeley สอนด้วย Scheme ซึ่งเป็นสำเนียงหนึ่งของ Lisp และมันทำให้ช็อกไปเลย
อย่างที่ Hofstadter พูด มันให้ความรู้สึกใกล้คณิตศาสตร์ที่สุด ทำให้นึกถึงวิชาทฤษฎีคณิตศาสตร์หลายวิชา และเป็นภาษาที่สวยงามภาษาแรกที่ผมค้นพบ
ผมชอบคำอ้างนี้เป็นพิเศษ: “Lisp และ Algol ถูกสร้างขึ้นรอบแกนกลางที่เป็นธรรมชาติ ราวกับเป็นสาขาหนึ่งของคณิตศาสตร์ แกนกลางของ Lisp มีความบริสุทธิ์ดุจผลึก ซึ่งไม่เพียงดึงดูดสุนทรียะ แต่ยังทำให้มันยืดหยุ่นกว่าภาษาอื่นๆ ส่วนใหญ่มาก”
แม้จากมุมของคนที่เคยอยากเป็นนักทอพอโลยีเชิงพีชคณิต Scheme ก็ใกล้เคียงกับการ แสดงออกอย่างสง่างามและย่อส่วนที่สุด ของสไตล์การเขียนโปรแกรมที่แพร่หลายในบรรดาภาษาแบบ dynamic typing และมี garbage collection โดยรวม
มันให้ความรู้สึกเหมือน “ทฤษฎี” ในแง่ที่ดูสมบูรณ์ และเมื่อคิดแนวทางแก้ปัญหาด้วย Scheme แล้วค่อยย้ายไปภาษา dynamic typing ภาษาอื่น ก็ยังไปถึงคำตอบที่สง่างามได้
Scheme เป็นภาษาที่ทำให้เรียบง่ายและจัดระเบียบขึ้นเมื่อเทียบกับ Lisp แบบดั้งเดิมในยุคนั้น เช่น lexical scope และ namespace เดียวสำหรับฟังก์ชันกับตัวแปร
ถ้าเป็นปี 1983 ก็เป็นเวลาประมาณ 20 ปีหลังจากสาขานี้เริ่มต้น แต่เขากำลังพูดถึง AI ที่โปรแกรมคอมพิวเตอร์ให้มี “ความยืดหยุ่น สามัญสำนึก การหยั่งเห็น ความคิดสร้างสรรค์ การตระหนักรู้ตนเอง และอารมณ์ขัน”
รายการนี้อ่านแล้วเหมือนเป็นรายชื่อสิ่งที่ LLM ทำได้แย่จริงๆ เลย จึงค่อนข้างน่าสนุก
ถึงอย่างนั้น อย่างน้อยก็ถือว่ามี ความคืบหน้าที่ไม่ใช่ศูนย์ ไปในทิศทางนั้นแล้ว
บางทีอาจจะมากกว่าด้วยซ้ำ แต่พูดแบบนั้นแล้วตัวผมเองก็รู้สึกแปลกๆ
แม้หลายอย่างจะยังไม่กลายเป็นผลลัพธ์ แต่ก็น่าอ่านอย่างน่าสนใจ
บทความร่วมอีกสองชิ้นที่ถูกกล่าวถึงพร้อมบทความนี้ อยู่ในบทที่ 17–19 ของหนังสือ Metamagical Themas ของ Hofstadter และยังมีบทความอื่นๆ จากคอลัมน์เดียวกันใน Scientific American รวมอยู่ด้วย
[0]: https://www.goodreads.com/book/show/181239.Metamagical_Thema...
ชื่อหนังสือมาจากชื่อคอลัมน์ที่เป็นแอนาแกรมของ “Mathematical Games” ซึ่ง Martin Gardner เคยเขียนใน Scientific American และ Hofstadter เป็นผู้รับช่วงต่อคอลัมน์นั้น
บทความบอกว่าการใช้
carหรือcdrกับnilควรเกิดข้อผิดพลาดเหมือนการหารด้วย 0 แต่ใน Lisp สมัยใหม่หลายกรณีไม่เป็นเช่นนั้นแล้วใน Lisp ดั้งเดิมที่ John McCarthy นิยามไว้
CARและCDRไม่ได้ถูกนิยามสำหรับNIL: <https://dl.acm.org/doi/pdf/10.1145/367177.367199>แต่ Common Lisp และ Emacs Lisp นิยาม
(car nil)และ(cdr nil)ให้เป็นnil: <https://www.lispworks.com/documentation/HyperSpec/Body/f_car...>, <https://www.gnu.org/software/emacs/manual/html_node/elisp/Li...>ใน
LISP 2156ค่า(status lispversion)ส่งกลับ/2156และทั้ง(car nil)กับ(cdr nil)ต่างก็ส่งกลับNILcarและcdrกลายมาเป็นแกนหลักในการกำเนิดภาษาที่ออกแบบเชิงคณิตศาสตร์ตัวดำเนินการพื้นฐานและมีชื่อเสียงที่สุดของ “List Processor” ไม่ได้ถูกสร้างมาให้ทำงานกับลิสต์ แต่กับ cons ซึ่งเป็นองค์ประกอบการแทนข้อมูลเฉพาะของเครื่องที่ Lisp ใช้สร้างโครงสร้างข้อมูล
cons ก็ไม่ได้ถูกตีความเป็นลิสต์เสมอไป และลิสต์ที่สำคัญมากซึ่งเป็นกรณีฐานของฟังก์ชันเรียกซ้ำบนลิสต์ ก็ไม่ได้แทนด้วย cons ด้วยซ้ำ
แม้ผ่านไป 60 ปี โปรแกรม Lisp ส่วนใหญ่ก็ยังเต็มไปด้วยการดำเนินการกับ cons และชื่อที่แม่นยำกว่านี้อาจเป็น “Cons Processor” ก็ได้
สิ่งนี้ทำให้นึกถึงข้อเท็จจริงที่ว่า Lisp ถือกำเนิดในยุคที่ภาษาและการใช้งานจริงต้องสอดประสานกัน และยิ่งทำให้ความสำเร็จในการวางรากฐานภาษาคอมพิวเตอร์บนตรรกะคณิตศาสตร์ดูน่าทึ่งยิ่งขึ้น
CARหรือCDRของnilจะเป็นข้อผิดพลาดcar/cdrกับnilเป็นข้อผิดพลาด ทำให้สำหรับมือใหม่อย่างผม โค้ดดูไม่ค่อยถูกหลักสรีรศาสตร์นักถึงอย่างนั้นก็ยังชอบ Guile Scheme มากกว่า Common Lisp อยู่ดี เลยรู้สึกเสียดาย
nilแล้วไม่เกิดข้อผิดพลาด แต่ส่งกลับnilอีกครั้งหรือไม่ทำให้นึกถึง SQL ด้วย และโดยส่วนตัวมองว่าเป็นทางเลือกที่ไม่ดี ถ้าไม่มีคำอื่นก็อยากเรียกว่า “bleeding nils/NULLs”
ยิ่งแย่ลงไปอีกหาก
nilไม่เท่ากับfalseในการเปรียบเทียบบูลีนใน Ruby และ Elixir
nilถูก扱เหมือนfalseและ Elixir แยกให้มีทั้งandที่รับเฉพาะบูลีนล้วน ๆ กับ&&ที่ถือว่าnilเป็นfalseการออกแบบแบบนี้อาจทำให้โค้ดดูสะอาดในตอนแรก แต่
nilที่ถูกจัดการผิดซึ่งเป็นข้อผิดพลาดจริง ๆ อาจไปปรากฏในส่วนที่ไม่เกี่ยวกันเลยหลังผ่าน call stack ไปหลายชั้น ทำให้ดีบักยากขึ้นมากข้อมูลเกี่ยวกับ Lisp ในบทความนี้ผมรู้อยู่แล้ว แต่ก็ยังอ่านสนุก Hofstadter มีลีลาการใช้คำที่มีเสน่ห์จริง ๆ
โดยเฉพาะมุกที่สร้าง
rejoyceซึ่งเป็นการดำเนินการย้อนกลับของreaders-digest-condensed-versionแล้วเมื่อรัน(rejoyce 'Stately 'Yes)นางฟ้า Lisp จะสร้าง Ulysses ทั้งเล่ม ที่เหมือน James Joyce น่าจะเขียนขึ้นมาตั้งแต่ต้น นั้นตลกดีใช้เวลาพอสมควร แต่สุดท้ายเราก็มาถึงจุดนั้นจนได้ และแม้ AI ในปี 2024 จะไม่เหมือนกับสิ่งที่เขาจินตนาการไว้ในปี 1983 อย่างสมบูรณ์ แต่การสร้างข้อความขึ้นใหม่จาก seed สั้น ๆ ก็เป็นงานที่เข้ากับ AI ทุกวันนี้ได้ค่อนข้างดี
มองว่า Lisp ยังคงเป็นหนึ่งในภาษาหลักที่สามารถรองรับแนวคิด ลูปประหลาด ที่ Hofstadter กล่าวถึงได้
Lisp ไม่ใช่ภาษาเดียวที่เป็นโฮโมไอโคนิค แต่ในบรรดาภาษาที่ผู้คนใช้งานเป็นจริง ๆ ก็ถือว่าอยู่ในกลุ่มที่ใหญ่ที่สุดซึ่ง
evalไม่ได้ต้องรับสตริงมาให้พาร์สไม่ชอบที่ผู้คนมักเหมารวม Lisp ว่าเท่ากับการเขียนโปรแกรมเชิงฟังก์ชันทั้งหมด
ไม่ได้เกลียดการเขียนโปรแกรมเชิงฟังก์ชัน แต่ ลักษณะเชิงสัญลักษณ์ ของ Lisp น่าสนใจกว่ามาก และการสร้างโค้ดที่ขับเคลื่อนด้วยส่วน
(go tag)เพื่อเขียนโปรแกรมแบบ GOTO ก็ทำได้ง่ายมาก ซึ่งเป็นเรื่องที่สนุกไม่รู้จบความสามารถด้านเมตาโปรแกรมมิงในรูปแบบที่ใช้ XSLT กับ XML เพื่อสร้าง XSLT แทนการเขียนโค้ดซ้ำ ๆ เอง เคยถูกใช้อย่างค่อนข้างแพร่หลาย
แต่ไวยากรณ์เป็นปัญหายิ่งกว่าไวยากรณ์ของ Lisp
การสร้างภาษาที่มีไวยากรณ์เหมาะกับการใช้งานประจำวัน และในขณะเดียวกันก็ไม่ลำบากเกินไปที่จะจัดการในฐานะ abstract syntax tree นั้นไม่ง่าย และ Lisp เป็นหนึ่งในตัวอย่างไม่กี่รายที่ค่อนข้างประสบความสำเร็จ
functoolsของ Python นั้นจริง ๆ แล้วคืออะไร: https://docs.python.org/3/library/functools.htmlกำลังสร้าง AGI เชิงสัญลักษณ์โดยไม่ใช้ Lisp และอยากฟังเบาะแสจากผู้เชี่ยวชาญ
ความสามารถฝั่ง Python ที่เข้าใจคือประมาณฟังก์ชันที่นำฟังก์ชันอื่นไปใช้กับ iterable อย่าง
filter(),map(),reduce(), แรปเปอร์ที่กำหนดเส้นทางการเรียกอย่าง@singledispatch, ฟังก์ชันที่ช่วยด้านการควบคุมโฟลว์หรือความสะดวกด้านประสิทธิภาพอย่าง@cacheและpartial(), รวมถึงความสามารถในการห่อฟังก์ชันตามอำเภอใจอย่างwraps()สิ่งเหล่านี้ส่วนใหญ่ดูเหมือนฟังก์ชันอำนวยความสะดวกสำหรับเรียกฟังก์ชันในรูปแบบพิเศษ และยังดูไม่ถึงระดับคำชื่นชม Lisp ที่ปฏิบัติกับ “การพิจารณาตัวเอง” เป็นเรื่องระดับ first-class
อยากรู้ว่า Lisp ให้สิ่งใดจริง ๆ ที่ความสามารถข้างต้นไม่มี
พวกเขาใช้ Lisp เป็นเครื่องมือถ่ายทอดแนวคิด และแนวคิดนั้นมักวนอยู่รอบการเรียกซ้ำเชิงฟังก์ชัน
Scheme และวัฒนธรรมรอบ ๆ ก็มีส่วนเช่นกัน
แม้ Scheme จะไม่ได้เป็นเพียงภาษาเชิงฟังก์ชัน แต่ก็เน้นการเขียนโปรแกรมแบบบริสุทธิ์มากกว่าสาย Lisp ก่อนหน้า และภาษาพื้นฐานให้โครงสร้างแบบ tail recursion แทนโครงสร้างการวนซ้ำ พร้อมทั้งกำหนดให้ implementation ต้องทำ tail call optimization
ใน Common Lisp ก็สามารถนิยาม
defaliasเป็นแมโครแล้วใช้เหมือนdefaliasของ Emacs ได้: https://stackoverflow.com/questions/24252539/defining-aliase...ชอบบทความเกี่ยวกับ Lisp ของ Hofstadter ใน Metamagical Themas และได้ย้ายโค้ดจากบทความสุดท้ายของคอลัมน์นั้นมาปรับจัดระเบียบให้เข้ากับ Clojure สำหรับกลุ่มศึกษาที่ทำงาน
[1] http://johnj.com/posts/oodles/