1 คะแนน โดย GN⁺ 2024-10-18 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • บทความของ 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 จะต้องมีอย่างน้อยหนึ่งองค์ประกอบ และองค์ประกอบแรกนั้นเรียกว่า car
    • car ของ (eval pi) คือ eval
    • car ของ (plus 2 2) คือ plus
  • car ไม่จำเป็นต้องเป็นชื่อฟังก์ชันเสมอไป
    • car ของ ((1)(2 2) (3 3 3)) คือ (1)
  • เมื่อตัด car ออกไป ลิสต์ที่เหลือเรียกว่า cdr
    • cdr ของ (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)
  • ปฏิบัติการย้อนกลับชื่อ 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 * k
    • cube คือ 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 ความคิดเห็น

 
GN⁺ 2024-10-18
ความคิดเห็นบน 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 ที่กลับด้าน
    • อาจเป็น ข้อผิดพลาดของ OCR ก็ได้
  • บทความนี้แสดงให้เห็นอีกครั้งว่า สาเหตุสำคัญที่ Lisp ไม่ค่อยได้รับความนิยมคือ วิธีอธิบาย Lisp
    บทความ Lisp แบบนี้ไม่ได้บอกอะไรที่ผมในวัยเด็กเอาไปใช้ได้จริงเลย และก็ไม่ได้แสดงให้เห็นว่างาน X บางอย่างจะง่ายขึ้นแค่ไหนเมื่อเทียบกับ Assembly/C/Pascal ฯลฯ
    ถ้าเป็นผมตอนนี้คงจะสาธิตว่า “ตัวตรวจสะกดที่ใช้เวลา 7 เดือนใน Assembly น่ะเหรอ? บนไมโครคอมพิวเตอร์ที่ใช้ bank-switched memory ถ้าใช้ Lisp แทบจะเป็นเรื่องเล็กน้อย และยังทำ garbage collection ให้ deterministic ได้แม้บน CPU ห่วยๆ”
    บทความและตำรา Lisp จำนวนมากเอาแต่พูดซ้ำเรื่องลิสต์ รีเคอร์ชัน และ AI แต่ไม่แสดงวิธีทำงานที่มีประโยชน์ ทำให้รู้สึกเสียดายเวลาราวกับเขียนโปรแกรมด้วยแหนบ เม็ดข้าว และกาว

    • Common Lisp: A Gentle Introduction to Symbolic Computation อาจมีประโยชน์ในบริบทนี้: https://www.cs.cmu.edu/~dst/LispBook/
    • ยังมี Practical Common Lisp ด้วย: https://gigamonkeys.com/book/
    • บทความถัดๆ มาของ Hofstadter มีตัวอย่างที่น่าสนใจกว่านี้
      นิตยสาร 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 มีความบริสุทธิ์ดุจผลึก ซึ่งไม่เพียงดึงดูดสุนทรียะ แต่ยังทำให้มันยืดหยุ่นกว่าภาษาอื่นๆ ส่วนใหญ่มาก”

    • สงสัยว่าเคยลองใช้ Haskell ไหม สำหรับผมมันให้ความรู้สึกใกล้คณิตศาสตร์ยิ่งกว่า เพราะประกอบด้วยนิยาม ไม่ใช่ขั้นตอน และหน้าตาภายนอกก็ดูเหมือนคณิตศาสตร์ด้วย
    • อาจเสี่ยงจะออกนอกประเด็นจากต้นฉบับเล็กน้อย แต่ผมคิดว่าการเรียกสิ่งนี้ว่า “คณิตศาสตร์” อาจทำให้ภาพพร่าลงด้วยซ้ำ
      แม้จากมุมของคนที่เคยอยากเป็นนักทอพอโลยีเชิงพีชคณิต Scheme ก็ใกล้เคียงกับการ แสดงออกอย่างสง่างามและย่อส่วนที่สุด ของสไตล์การเขียนโปรแกรมที่แพร่หลายในบรรดาภาษาแบบ dynamic typing และมี garbage collection โดยรวม
      มันให้ความรู้สึกเหมือน “ทฤษฎี” ในแง่ที่ดูสมบูรณ์ และเมื่อคิดแนวทางแก้ปัญหาด้วย Scheme แล้วค่อยย้ายไปภาษา dynamic typing ภาษาอื่น ก็ยังไปถึงคำตอบที่สง่างามได้
      Scheme เป็นภาษาที่ทำให้เรียบง่ายและจัดระเบียบขึ้นเมื่อเทียบกับ Lisp แบบดั้งเดิมในยุคนั้น เช่น lexical scope และ namespace เดียวสำหรับฟังก์ชันกับตัวแปร
  • ถ้าเป็นปี 1983 ก็เป็นเวลาประมาณ 20 ปีหลังจากสาขานี้เริ่มต้น แต่เขากำลังพูดถึง AI ที่โปรแกรมคอมพิวเตอร์ให้มี “ความยืดหยุ่น สามัญสำนึก การหยั่งเห็น ความคิดสร้างสรรค์ การตระหนักรู้ตนเอง และอารมณ์ขัน”
    รายการนี้อ่านแล้วเหมือนเป็นรายชื่อสิ่งที่ LLM ทำได้แย่จริงๆ เลย จึงค่อนข้างน่าสนุก
    ถึงอย่างนั้น อย่างน้อยก็ถือว่ามี ความคืบหน้าที่ไม่ใช่ศูนย์ ไปในทิศทางนั้นแล้ว

    • เมื่อเทียบกับคนทั่วไปที่ผมเจอ LLM ดูเหมือนจะแสดง สามัญสำนึก การหยั่งเห็น ความคิดสร้างสรรค์ การตระหนักรู้ตนเอง และอารมณ์ขัน ได้พอๆ กับคนเฉลี่ย
      บางทีอาจจะมากกว่าด้วยซ้ำ แต่พูดแบบนั้นแล้วตัวผมเองก็รู้สึกแปลกๆ
    • กลุ่มวิจัยของ Hofstadter มีประวัติยาวนานในการพยายามจัดการปัญหาเหล่านี้
      แม้หลายอย่างจะยังไม่กลายเป็นผลลัพธ์ แต่ก็น่าอ่านอย่างน่าสนใจ
  • บทความร่วมอีกสองชิ้นที่ถูกกล่าวถึงพร้อมบทความนี้ อยู่ในบทที่ 17–19 ของหนังสือ Metamagical Themas ของ Hofstadter และยังมีบทความอื่นๆ จากคอลัมน์เดียวกันใน Scientific American รวมอยู่ด้วย
    [0]: https://www.goodreads.com/book/show/181239.Metamagical_Thema...
    ชื่อหนังสือมาจากชื่อคอลัมน์ที่เป็นแอนาแกรมของ “Mathematical Games” ซึ่ง Martin Gardner เคยเขียนใน Scientific American และ Hofstadter เป็นผู้รับช่วงต่อคอลัมน์นั้น

    • ผมชอบหนังสือเล่มนี้มาก ขอแนะนำอย่างยิ่งสำหรับคนที่ชอบงานคลาสสิกของ Hofstadter อย่าง Gödel, Escher, Bach
    • หนังสือเล่มนี้งดงามมากทั้งเวลาอ่านและเวลามองดู และมีภาพประกอบที่น่าสนใจมากมาย
  • บทความบอกว่าการใช้ 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...>

    • สงสัยว่าใน Maclisp เป็นอย่างไร เลยลองเชื่อมต่อ telnet ไปยัง ITS สาธารณะของ Lars Brinkhoff เพื่อตรวจสอบดู
      ใน LISP 2156 ค่า (status lispversion) ส่งกลับ /2156 และทั้ง (car nil) กับ (cdr nil) ต่างก็ส่งกลับ NIL
    • น่าประหลาดใจที่รายละเอียดการใช้งานเฉพาะเจาะจงมากอย่าง car และ cdr กลายมาเป็นแกนหลักในการกำเนิดภาษาที่ออกแบบเชิงคณิตศาสตร์
      ตัวดำเนินการพื้นฐานและมีชื่อเสียงที่สุดของ “List Processor” ไม่ได้ถูกสร้างมาให้ทำงานกับลิสต์ แต่กับ cons ซึ่งเป็นองค์ประกอบการแทนข้อมูลเฉพาะของเครื่องที่ Lisp ใช้สร้างโครงสร้างข้อมูล
      cons ก็ไม่ได้ถูกตีความเป็นลิสต์เสมอไป และลิสต์ที่สำคัญมากซึ่งเป็นกรณีฐานของฟังก์ชันเรียกซ้ำบนลิสต์ ก็ไม่ได้แทนด้วย cons ด้วยซ้ำ
      แม้ผ่านไป 60 ปี โปรแกรม Lisp ส่วนใหญ่ก็ยังเต็มไปด้วยการดำเนินการกับ cons และชื่อที่แม่นยำกว่านี้อาจเป็น “Cons Processor” ก็ได้
      สิ่งนี้ทำให้นึกถึงข้อเท็จจริงที่ว่า Lisp ถือกำเนิดในยุคที่ภาษาและการใช้งานจริงต้องสอดประสานกัน และยิ่งทำให้ความสำเร็จในการวางรากฐานภาษาคอมพิวเตอร์บนตรรกะคณิตศาสตร์ดูน่าทึ่งยิ่งขึ้น
    • Scheme ไม่เป็นเช่นนั้น การเอา CAR หรือ CDR ของ nil จะเป็นข้อผิดพลาด
    • น่าเสียดายที่ใน Scheme การใช้ 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 ก็ทำได้ง่ายมาก ซึ่งเป็นเรื่องที่สนุกไม่รู้จบ

    • อีกภาษาหนึ่งที่เป็นโฮโมไอโคนิคและเป็นฟังก์ชันจริง ๆ ซึ่งเคยได้รับการใช้งานกระแสหลักช่วงสั้น ๆ ในทศวรรษ 2000 คือ XSLT
      ความสามารถด้านเมตาโปรแกรมมิงในรูปแบบที่ใช้ XSLT กับ XML เพื่อสร้าง XSLT แทนการเขียนโค้ดซ้ำ ๆ เอง เคยถูกใช้อย่างค่อนข้างแพร่หลาย
      แต่ไวยากรณ์เป็นปัญหายิ่งกว่าไวยากรณ์ของ Lisp
      การสร้างภาษาที่มีไวยากรณ์เหมาะกับการใช้งานประจำวัน และในขณะเดียวกันก็ไม่ลำบากเกินไปที่จะจัดการในฐานะ abstract syntax tree นั้นไม่ง่าย และ Lisp เป็นหนึ่งในตัวอย่างไม่กี่รายที่ค่อนข้างประสบความสำเร็จ
    • โดยเฉพาะเมื่อไปถึง การเขียนโปรแกรมแบบอิง continuation ของ Scheme เส้นแบ่งระหว่างการเขียนโปรแกรมเชิงฟังก์ชันกับไม่ใช่เชิงฟังก์ชันก็เลือนรางจนแทบไม่มีความหมาย
    • อยากรู้ว่า การรองรับโฮโมไอโคนิค/สัญลักษณ์ ในระดับลึกที่โปรแกรมเมอร์ปี 2024 ได้จาก 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 ผ่านแวดวงวิชาการเท่านั้น และนักวิชาการที่สอน Lisp เองก็ไม่ได้สนใจจะพัฒนาอะไรจริง ๆ ด้วย 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/

    • ก็สงสัยด้วยว่ามีฉบับแปลที่ย้ายไปเป็น Lisp สมัยใหม่แล้วหรือไม่