3 คะแนน โดย GN⁺ 2024-05-03 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

แนะนำภาษาโปรแกรม Cognition

ประเด็นปัญหา

  • นักเขียนภาษา Lisp อ้างว่าสามารถสร้างการเมตาโปรแกรมมิ่งและระบบทั่วไปด้วยโค้ดแบบ S-expression และระบบแมโครเชิงฟังก์ชัน
  • แต่ Lisp ก็มีปัญหาพื้นฐาน
    • การทำเมตาโปรแกรมมิ่งไม่เหมือนกับการเขียนโปรแกรมทั่วไป จึงทำให้ Lisp ต้องมีไวยากรณ์ที่ค่อนข้างตายตัวเสมอ (เช่น เครื่องหมายวงเล็บหรืออักขระสำหรับ look-ahead)
    • วงเล็บเปิดบอกให้อ่านต่อไปจนกว่าจะพบวงเล็บปิดที่ตรงกัน
    • ดังนั้นอักขระวงเล็บซ้าย/ขวาจึงกลายเป็นสิ่งที่ไม่สามารถปรับเปลี่ยนได้ภายในภาษา (แม้ในเชิงแนวคิดอาจไม่เป็นเช่นนั้น แต่ในบางการนำไปใช้ก็ไม่สามารถทำได้)
    • ที่สำคัญกว่านั้น การเปลี่ยนลำดับการจัดประเภทอักขระเหล่านี้ภายหลังโดยไม่พึ่งการจัดการสตริงแทบเป็นไปไม่ได้
  • ภาษาตัวอื่นๆ ก็มีวิธีการอื่นในการตัดสินใจว่าจะอ่านข้อมูลต่อไปจากอักขระใดบ้าง
    • ขั้นตอนนี้คือไวยากรณ์ (syntax)
  • Cognition ใช้ antisyntax แบบ postfix แบบครบวงจร จึงต่างออกไป
    • คล้ายกับภาษาประเภท concatenative แต่ภาษาประเภท concatenative ก็มีปัญหาสองข้อสำคัญ
      1. การนำอักขระวงเล็บซ้าย/ขวาเข้ามา (ซึ่งจริงๆแล้วเป็น prefix notation)
      2. อักขระอัญประกาศสำหรับสตริง
    • จึงไม่เหมาะกับการเป็นภาษาโปรแกรมแบบทั่วไป
    • ในการนำไปใช้งานไวยากรณ์แบบ C ของ Lisp ก็พบปัญหาแบบเดียวกัน (มีการใช้ escape character มากเกินไป และต้องพึ่งอักขระช่องว่างเพื่อแยกจุดเริ่มต้น/จุดสิ้นสุดของโทเค็นบางตัว)
  • Racket มีระบบแมโคร แต่ไม่ทำงานอย่าง dynamic ที่ runtime และใช้การ preprocess

แนะนำ Cognition

  • เป็นโปรเจกต์ที่ทำงานร่วมกับ Matthew Hinton มาหลายเดือน
  • เป้าหมายคือการทำให้เกิดระบบไวยากรณ์ที่ยืดหยุ่นที่สุดในหนึ่งในระบบที่รู้จัก โดยใช้ postfix แบบครบถ้วน
  • อาจต้องมีความรู้พื้นฐานด้าน grammar/tokenization/parsing แต่พยายามอธิบายให้เข้าใจง่ายที่สุดเท่าที่ทำได้
  • ที่เก็บโค้ด: https://github.com/metacrank/cognition

Baremetal Cognition

  • Baremetal Cognition คล้ายกับ Brainfuck แต่สามารถทำเมตาโปรแกรมมิ่งขั้นสูงได้
  • ตัวอย่างโค้ด bootstrap:
ldfgldftgldfdtgldf dfiff1 crank f
  • ช่องว่างและการขึ้นบรรทัดใหม่มีความสำคัญมาก
    • บรรทัดที่ 2 คือมีช่องว่างหลัง df
    • บรรทัดที่ 3 มีอักขระช่องว่าง
  • จากนั้นจึงสามารถแนะนำแนวคิดใหม่อีก 2 อย่างคือ delimiter และ ignore

Tokenization

  • Delimiter ทำให้ tokenizer แยกจุดเริ่มต้นและจุดสิ้นสุดของโทเค็นได้
  • รายการ tokenizer อักขระตัวเดียวเป็นที่สาธารณะ และสามารถแก้ไข/อ่านได้ภายใน Cognition
  • อักขระที่ถูก ignore จะถูก tokenizer ข้ามทั้งหมดในขั้นตอนแรกของ read-eval-print loop เสมอ
    • กล่าวคือ ข้ามชุดอักขระ ignore ที่ตั้งไว้เมื่อเริ่มเก็บโทเค็น
  • โดยปกติ ทุกอักขระเป็น delimiter และไม่มีอักขระ ignored
  • ด้วยรายการ delimiter และ ignored-character สามารถสลับโหมด blacklist/whitelist ได้สำหรับอักขระเหล่านี้ (เพื่อให้กระชับและใช้งานได้จริง)

Falias

  • Falias คือรายการคำที่เมื่อถูกวางลง stack จะถูก execute
  • Falias ทุกตัวจะ execute ค่าบนสุดของ stack (เทียบได้กับ eval ใน Stem)
  • f เป็น Falias พื้นฐาน: มัน execute stack top d โดยไม่ต้องวาง f ลงสแตก
  • d แปลง delimiter list ให้เป็นค่า string ของคำ (หมายความว่า l จะถูกยกเว้นจาก delimiter)
  • ในสภาพแวดล้อมมาตรฐาน คำใดๆ จะไม่ถูก execute ยกเว้น Falias พิเศษ

ข้อสังเกตเรื่อง delimiter

  • delimiter มีกฎที่น่าสนใจ
    • หากอักขระไม่ได้เป็น ignore ในลูป tokenize อักขระ delimiter จะถูกใส่ต่อเข้ากับโทเค็นปัจจุบันและเก็บต่อไป
    • ตรงนี้ตรงข้ามกับ singlet (ซึ่งจะใส่ตัวเองแล้วข้ามเพื่อจบการเก็บโทเคน)
  • สามารถกำหนดได้ว่าถูก blacklist หรือ whitelist
    • สามารถ blacklist/whitelist รายการ delimiter, singlet และ ignored character
    • โดยเริ่มต้นมี blacklist ของ delimiter, whitelist ของ singlet และ whitelist ของ ignored character เท่ากับไม่มีเลย
  • อักขระอื่นๆ ทั้งหมดจะถูกเก็บเป็นส่วนหนึ่งของโทเค็นปัจจุบันต่อเนื่องกัน จนกว่า loop จะหยุดโดยกฎ delimiter หรือ singlet

โค้ด bootstrap ต่อ

ldf
  • ทำให้ l ไม่เป็น delimiter
gldftgldfdtgldf  dfiff1 crank f
  • เนื่องจาก d เป็น delimiter จึง gl ถูกวางบน stack และเรียกใช้งาน Falias f เพื่อทำให้ gl ไม่เป็น delimiter
  • tgl ถูกวางบน stack และ df ทำให้ tgl กลายเป็นอักขระที่ไม่เป็น delimiter
  • dtgl ถูกวางบน stack และ \ndf ทำให้ \n (newline) เป็นอักขระที่ไม่เป็น delimiter (newline อยู่จริงๆในโค้ด)
  • ตามกฎ delimiter จึงใส่ช่องว่างและ \n ลง stack (รวมช่องว่างในบรรทัดที่ 3)
  • โทเค็น \n จะถูกสร้างอีกตัว
  • stack ตอนนี้เป็นดังนี้ (จากล่างขึ้นบน): 3. dtgl 2. [อักขระช่องว่าง]\n
    1. [อักขระช่องว่าง]\n
  • df ตั้งค่า \n ให้เป็นอักขระที่ไม่ใช่ delimiter
  • if ตั้ง \n เป็นอักขระ ignore (ข้ามตอนเริ่ม tokenization)
  • f execute dtgl เพื่อสลับโหมด dflag ระหว่าง whitelist/blacklist ของ delimiter
  • ตอนนี้อักขระที่ไม่ใช่ delimiter ทั้งหมดกลายเป็น delimiter และอักขระ delimiter ทั้งหมดกลายเป็นไม่ใช่ delimiter
  • สุดท้ายช่องว่างกับ newline กลายเป็น delimiter ของโทเค็น และถูก ignore เมื่อเริ่ม tokenization
  • จากนั้น 1 จะถูก tokenize และวางบน stack, จากนั้น crank ถูก tokenize และถูก execute โดย f (1 ในกรณีนี้ถือเป็นตัวเลข แต่ใน Cognition ทุกอย่างเป็นคำ)
  • การ bootstrap sequence เสร็จสมบูรณ์แล้ว! สิ่งที่ crank ทำจะอธิบายในส่วนต่อไป

สรุป bootstrap

  • Cognition สามารถเปลี่ยนกฎ tokenization แบบ dynamic ได้ด้วยการเขียนโปรแกรม
    • เรื่องนี้ทำไม่ได้ในภาษาอื่น
    • สามารถเขียน tokenizer สำหรับภาษาภายนอกภายใน Cognition และ tokenize ตามที่ต้องการได้
  • เนื่องจากใช้ postfix และไม่ใช้ look-ahead ทำให้ทำได้
    • ไม่ต้อง parse โทเค็นมากกว่าหนึ่งตัวก่อนประเมิน expression
  • ด้วย Falias คุณสามารถ execute คำได้โดยไม่ต้องเริ่มจากคำต้นทางหรือตัว keyword เริ่มต้น

Crank

  • ระบบ metacrank ช่วยกำหนดวิธีเริ่มต้นสำหรับการ execute คำใน stack
  • คำสั่ง crank รับตัวเลขเป็นอาร์กิวเมนต์ และจะ execute stack top โดยอัตโนมัติเมื่อมีการใส่คำใน stack n คำ
  • ตัวอย่างโค้ด (crank 1 คือค่าเริ่มต้น):
5 crank 2
crank 2 crank 
1 crank unglue swap quote prepose def
  • ในสภาพแวดล้อม crank 1 สามารถหยุดการใช้ f ระหว่างการประเมินโทเค็น
    • ประเมินหนึ่งคำต่อหนึ่ง token ที่ถูก tokenize
    • เพราะโปรแกรม syntax ที่ใช้ช่องว่างและ newline ทำให้ตีความโค้ดได้อย่างเป็นธรรมชาติ
  • โค้ดเริ่มจากการพยายามประเมิน 5 (ไม่ใช่ builtin จึงประเมินตัวเอง)
  • crank ถูกตั้งค่าให้ execute เมื่อมีการใส่ 5 คำลงสแต็ค
  • 2crank, 2, crank, 1 ถูกวางบน stack ทั้งหมด (เพราะตั้งค่า crank 5 ทำให้ crank เป็น builtin และจึงไม่ถูก execute): 4. 2crank 3. 2 2. crank
    1. 1
  • crank เป็นคำที่ 5 จึงถูก execute (ตั้งไว้ crank 1)
  • unglue เป็น builtin ที่ดึงค่าของคำบนสุดของ stack (ผ่าน 1 ที่เรียกใช้)
    • คือดึง function pointer ที่ผูกกับ builtin crank
  • Stack มีลักษณะดังนี้: 3. 2crank 2. 2
    1. [CLIB]
    • CLIB คือ function pointer ที่ชี้ไปยัง builtin crank
  • การ execute swap: 3. 2crank 2. [CLIB]
    1. 2
  • execute quote (builtin ที่ quote ค่าบนสุด): 3. 2crank 2. [CLIB]
    1. [2]
  • execute prepose (คล้าย compose ของ Stem แต่เป็นการวางไว้หน้าสุดและเรียกว่า VMACRO) 2. 2crank
    1. ([2] [CLIB])
  • เรียก def
    • ใส่ 2 ลง stack และนิยามคำ 2crank ให้เรียก function pointer ของ builtin crank
  • ต้องอธิบายเพิ่มเติมว่า VMACRO คืออะไร และความแตกต่างระหว่าง stack ของ Cognition กับ Stem stack

ความต่างกับ Stem

  • ใน Stem stack คำสามารถวางลง stack ได้โดยตรง
  • ใน Cognition คำจะไม่ถูกประเมิน แต่ถูกใส่ไว้ใน container แล้วค่อยวางใน stack
    • ใน Stem compose แบบคำสั่งทำงานกับคำ (หรือ container ที่มีคำเดียว) กับ container อื่น
    • สิ่งนี้ช่วยให้ API ของ Cognition สอดคล้องกันมากขึ้น
  • คำเช่น cd ก็นำแนวคิดนี้ไปใช้ด้วย

Macro

  • อีกความต่างอีกอย่างหนึ่งเมื่อเทียบกับ Stem quote และ container ของ Cognition
  • เมื่อประเมิน macro สิ่งทั้งหมดใน macro จะถูก evaluate และ crank จะถูก ignore
  • เมื่อคำถูกผูกแล้ว เวลาเช็ก/ประเมินคำนั้น

1 ความคิดเห็น

 
GN⁺ 2024-05-03
ความคิดเห็นบน Hacker News

สรุปความคิดเห็นสำคัญบางข้อได้ดังนี้:

  • ในส่วนนำของเอกสาร การอธิบายเกี่ยวกับโปรเจกต์ Cognition ปรากฏช้ากว่าที่ควร ดังนั้นควรนำเสนอประเด็นที่สำคัญที่สุดก่อนเพื่อประหยัดเวลาให้ผู้อ่าน
  • เช่นเดียวกับฟังก์ชันการตั้งค่าเลเยอร์ reader ของ Racket ซึ่งมีวิธีการอื่น ๆ ที่ขยายไวยากรณ์ได้แต่ยังคงความเข้ากันได้ระหว่างระบบได้อยู่แล้ว จึงมีข้อสงสัยว่าแนวทางของ Cognition จะ "ดีกว่า" มากขึ้นอย่างแท้จริงหรือไม่
  • Common Lisp ก็สามารถเปลี่ยนแปลงไวยากรณ์ได้อย่างอิสระด้วย reader macro, macro, compiler macro ได้ และแก่นของเมตาโปรแกรมมิงอยู่ที่การจัดการเชิงความหมาย ไม่ใช่ไวยากรณ์
  • ความสามารถของ Cognition ที่สามารถกำหนด กำหนดใหม่โครงสร้างไวยากรณ์ใน runtime และเข้า/ออกจากการทำงานได้เป็นสิ่งที่สวยงามและน่าสนใจ เพราะเป็นการเปิดประตูสู่โอกาสในการสร้าง "เครื่องจักรที่คิด" อย่างแท้จริง
  • เนื่องจากไวยากรณ์ทำหน้าที่เป็นโครงสร้าง การขจัดตัวไวยากรณ์ออกไปจึงเป็นการขัดแย้งกันเอง และไวยากรณ์ที่สั้นเกินไปอาจทำให้การอ่านและความเข้าใจถดถอยลงได้
  • ตัวการเขียนของเอกสารเองก็มีแนวทางที่ค่อนข้างรบกวนและให้ความรู้สึกเสียดสี ทำให้การอ่านยากขึ้น แต่ก็มีเนื้อหาลึกซึ้ง