เสียงกระเพื่อมโปรแกรมเมอร์โม้ว่าเสียงกระเพื่อมเป็นภาษาที่มีประสิทธิภาพซึ่งสามารถสร้างขึ้นจากชุดขนาดเล็กมากของการดำเนินงานดั้งเดิม tinylisp
ขอนำความคิดที่ว่าในการปฏิบัติโดยการเล่นกอล์ฟล่ามภาษาที่เรียกว่า
ข้อกำหนดภาษา
ในสเปคนี้เงื่อนไขใด ๆ ที่ผลลัพธ์ถูกอธิบายว่า "ไม่ได้กำหนด" อาจทำอะไรก็ได้ในล่ามของคุณ: ล้มเหลวล้มเหลวอย่างเงียบ ๆ สร้าง gobbldegook แบบสุ่มหรือทำงานตามที่คาดไว้ การดำเนินการอ้างอิงในหลาม 3 สามารถใช้ได้ที่นี่
วากยสัมพันธ์
ราชสกุลใน tinylisp มี(
, )
หรือสตริงใด ๆ ของหนึ่งหรือพิมพ์ตัวอักษร ASCII ยกเว้นวงเล็บหรือพื้นที่ (เช่น regex ต่อไปนี้: [()]|[^() ]+
.) โทเค็นใด ๆ ที่ประกอบด้วยตัวเลขทั้งหมดเป็นตัวอักษรจำนวนเต็ม (ศูนย์ชั้นนำจะถูก.) โทเค็นใด ๆ ที่มีตัวเลขที่ไม่ใช่เป็นสัญลักษณ์แม้ตัวอย่างตัวเลขที่ดูชอบ123abc
, และ3.14
-10
ช่องว่างทั้งหมด (รวมถึงอย่างน้อยที่สุดตัวอักษร ASCII 32 และ 10) จะถูกละเว้นยกเว้นตราบเท่าที่มันแยกโทเค็น
โปรแกรม tinylisp ประกอบด้วยชุดของการแสดงออก แต่ละนิพจน์เป็นจำนวนเต็มสัญลักษณ์หรือเอส - นิพจน์ (รายการ) รายการประกอบด้วยนิพจน์เป็นศูนย์หรือมากกว่าที่อยู่ในวงเล็บ ไม่มีการใช้ตัวคั่นระหว่างรายการ นี่คือตัวอย่างของการแสดงออก:
4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))
นิพจน์ที่ไม่ได้อยู่ในรูปแบบที่ดี (โดยเฉพาะอย่างยิ่งที่มีวงเล็บที่ไม่ตรงกัน) ให้พฤติกรรมที่ไม่ได้กำหนด (การใช้งานการอ้างอิงอัตโนมัติปิด parens ที่เปิดอยู่และหยุดการแยกวิเคราะห์ parens ใกล้เคียงที่ไม่ตรงกัน)
ชนิดข้อมูล
ชนิดข้อมูลของ tinylisp เป็นจำนวนเต็มสัญลักษณ์และรายการ ฟังก์ชั่นและมาโครในตัวสามารถจัดเป็นประเภทได้แม้ว่ารูปแบบเอาต์พุตจะไม่ได้กำหนด รายการสามารถมีค่าจำนวนเท่าใดก็ได้ทุกประเภทและสามารถซ้อนกันได้ลึกโดยพลการ จำนวนเต็มต้องได้รับการสนับสนุนอย่างน้อยตั้งแต่ -2 ^ 31 ถึง 2 ^ 31-1
รายการว่าง - ()
ยังเรียกว่าไม่มี - และจำนวนเต็ม0
เป็นค่าเดียวที่ถือว่าเป็นเท็จตรรกะ; จำนวนเต็มอื่น ๆ ทั้งหมด, รายการที่ไม่มีข้อ จำกัด , บิลด์อินและสัญลักษณ์ทั้งหมดเป็นจริงตามหลักเหตุผล
การประเมินผล
นิพจน์ในโปรแกรมจะถูกประเมินตามลำดับและผลลัพธ์ของแต่ละรายการที่ส่งไปยัง stdout (เพิ่มเติมเกี่ยวกับการจัดรูปแบบเอาต์พุตในภายหลัง)
- ตัวอักษรจำนวนเต็มประเมินตัวเอง
- รายการที่ว่างเปล่า
()
ประเมินเป็นของตัวเอง - รายการหนึ่งรายการขึ้นไปจะประเมินรายการแรกและถือเป็นฟังก์ชั่นหรือมาโครเรียกใช้รายการที่เหลือเป็นอาร์กิวเมนต์ หากรายการนั้นไม่ใช่ฟังก์ชัน / มาโครพฤติกรรมนั้นจะไม่ได้กำหนดไว้
- สัญลักษณ์ประเมินว่าเป็นชื่อโดยให้ค่ากับชื่อนั้นในฟังก์ชันปัจจุบัน หากไม่ได้กำหนดชื่อไว้ในฟังก์ชั่นปัจจุบันชื่อนั้นจะประเมินเป็นค่าที่เชื่อมโยงกับขอบเขตทั่วโลก หากไม่ได้กำหนดชื่อไว้ที่ขอบเขตปัจจุบันหรือระดับโลกผลลัพธ์จะไม่ได้กำหนดไว้ (การใช้การอ้างอิงจะให้ข้อความแสดงข้อผิดพลาดและส่งคืนค่าศูนย์)
ฟังก์ชั่นและมาโครในตัว
มีฟังก์ชันในตัวเจ็ดรายการใน tinylisp ฟังก์ชั่นประเมินแต่ละข้อโต้แย้งของมันก่อนที่จะใช้การดำเนินการบางอย่างกับพวกเขาและส่งกลับผลลัพธ์
c
- cons [รายการ truct] รับอาร์กิวเมนต์สองค่าและรายการและส่งคืนรายการใหม่ที่ได้รับโดยการเพิ่มค่าที่ด้านหน้าของรายการh
- หัว ( รถยนต์ในคำศัพท์ Lisp) นำรายการและส่งคืนไอเท็มแรกที่อยู่ในนั้นหรือไม่มีถ้ากำหนดเป็นศูนย์t
- tail ( cdrในคำศัพท์ Lisp) รับรายการและส่งคืนรายการใหม่ที่มีทั้งหมดยกเว้นรายการแรกหรือไม่มีถ้ากำหนดเป็นศูนย์s
- ลบ รับจำนวนเต็มสองจำนวนและส่งกลับค่าเป็นลบแรกl
- น้อยกว่า. ใช้จำนวนเต็มสองจำนวน ผลตอบแทนที่ 1 ถ้าแรกน้อยกว่าที่สองเป็น 0 มิฉะนั้นe
- เท่ากัน. รับค่าสองประเภทเดียวกัน (ทั้งจำนวนเต็มทั้งสองรายการหรือสัญลักษณ์ทั้งสอง) ส่งกลับ 1 ถ้าทั้งสองมีค่าเท่ากัน (หรือเหมือนกันในทุกองค์ประกอบ), 0 มิฉะนั้น การทดสอบบิลด์อินเพื่อความเท่าเทียมกันนั้นไม่ได้ถูกกำหนดไว้v
- eval ใช้เวลาหนึ่งรายการจำนวนเต็มหรือสัญลักษณ์แสดงถึงการแสดงออกและประเมินมัน เช่นการทำเช่น(v (q (c a b)))
เดียวกับการทำ(c a b)
; จะช่วยให้(v 1)
1
"ค่า" ที่นี่รวมถึงรายการจำนวนเต็มสัญลักษณ์หรือบิวด์อินเว้นแต่จะระบุไว้เป็นอย่างอื่น หากฟังก์ชั่นถูกระบุว่ารับชนิดที่เฉพาะเจาะจงการส่งผ่านประเภทที่แตกต่างนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดเช่นเดียวกับการส่งผ่านจำนวนอาร์กิวเมนต์ที่ไม่ถูกต้อง
มีแมโครในตัวสามตัวใน tinylisp แมโครซึ่งแตกต่างจากฟังก์ชั่นไม่ได้ประเมินข้อโต้แย้งของมันก่อนที่จะใช้การดำเนินการกับพวกเขา
q
- อ้าง รับหนึ่งนิพจน์และส่งคืนโดยไม่ประเมินค่า เช่นการประเมินผล(1 2 3)
ให้ข้อผิดพลาดเพราะมันพยายามที่จะเรียก1
ว่าเป็นฟังก์ชั่นหรือแมโคร แต่ผลตอบแทนที่รายการ(q (1 2 3))
(1 2 3)
การประเมินa
จะให้ค่ากับชื่อa
แต่(q a)
ให้ชื่อเองi
- ถ้า ใช้สามนิพจน์: เงื่อนไขนิพจน์ iftrue และนิพจน์ iffalse ประเมินเงื่อนไขก่อน หากผลลัพธ์นั้นเป็นเท็จ (0
หรือไม่มี) ให้ประเมินและส่งคืนนิพจน์ iffalse มิฉะนั้นประเมินและส่งคืนนิพจน์ iftrue โปรดทราบว่านิพจน์ที่ไม่ได้ส่งคืนจะไม่ถูกประเมินd
- def ใช้สัญลักษณ์และการแสดงออก ประเมินค่านิพจน์และผูกกับสัญลักษณ์ที่กำหนดซึ่งถือว่าเป็นชื่อที่ขอบเขตทั่วโลกจากนั้นส่งคืนสัญลักษณ์ การพยายามกำหนดชื่อซ้ำควรล้มเหลว (เงียบ ๆ ด้วยข้อความหรือล้มเหลวการใช้การอ้างอิงจะแสดงข้อความแสดงข้อผิดพลาด) หมายเหตุ: มันไม่จำเป็นที่จะต้องพูดชื่อก่อนที่จะผ่านมันไปd
แม้ว่ามันจะเป็นสิ่งที่จำเป็นที่จะพูดการแสดงออกว่าเป็นรายการหรือสัญลักษณ์ที่คุณไม่ต้องการประเมิน:(d x (q (1 2 3)))
เช่น
การส่งจำนวนอาร์กิวเมนต์ที่ไม่ถูกต้องไปยังแมโครนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ (การใช้งานการอ้างอิงล้มเหลว) การส่งสิ่งที่ไม่ใช่สัญลักษณ์เนื่องจากอาร์กิวเมนต์แรกของd
คือพฤติกรรมที่ไม่ได้กำหนด (การใช้การอ้างอิงไม่ได้ให้ข้อผิดพลาด แต่ไม่สามารถอ้างอิงค่าได้ในภายหลัง)
ฟังก์ชันและมาโครที่ผู้ใช้กำหนด
เริ่มต้นจากบิวด์อินทั้งสิบภาษาสามารถขยายได้โดยการสร้างฟังก์ชันและมาโครใหม่ สิ่งเหล่านี้ไม่มีประเภทข้อมูลเฉพาะ มันเป็นเพียงรายการที่มีโครงสร้างที่แน่นอน:
- ฟังก์ชั่นคือรายการของสองรายการ แรกคือรายการของชื่อพารามิเตอร์หนึ่งชื่อขึ้นไปหรือชื่อเดียวซึ่งจะได้รับรายการของอาร์กิวเมนต์ใด ๆ ที่ส่งผ่านไปยังฟังก์ชัน (ดังนั้นจึงอนุญาตให้ใช้สำหรับฟังก์ชัน ที่สองคือการแสดงออกซึ่งเป็นร่างกายฟังก์ชั่น
- แมโครเหมือนกับฟังก์ชันยกเว้นว่ามี nil อยู่ข้างหน้าชื่อพารามิเตอร์จึงทำให้เป็นรายการสามรายการ (การพยายามเรียกรายการสามรายการที่ไม่ได้เริ่มต้นด้วยศูนย์คือพฤติกรรมที่ไม่ได้กำหนดการใช้การอ้างอิงจะข้ามอาร์กิวเมนต์แรกและถือเป็นแมโครเช่นกัน)
ตัวอย่างเช่นนิพจน์ต่อไปนี้เป็นฟังก์ชันที่เพิ่มจำนวนเต็มสองจำนวน:
(q List must be quoted to prevent evaluation
(
(x y) Parameter names
(s x (s 0 y)) Expression (in infix, x - (0 - y))
)
)
และมาโครที่รับจำนวนอาร์กิวเมนต์ใด ๆ และประเมินและส่งกลับค่าแรก:
(q
(
()
args
(v (h args))
)
)
สามารถเรียกใช้ฟังก์ชันและแมโครโดยตรงผูกกับชื่อที่ใช้d
และส่งผ่านไปยังฟังก์ชันหรือแมโครอื่น
เนื่องจากฟังก์ชันของฟังก์ชันไม่ได้ถูกเรียกใช้งานในเวลาที่กำหนดฟังก์ชันเรียกซ้ำจึงสามารถนิยามได้ง่าย:
(d len
(q (
(list)
(i list If list is nonempty
(s 1 (s 0 (len (t list)))) 1 - (0 - len(tail(list)))
0 else 0
)
))
)
อย่างไรก็ตามโปรดทราบว่าข้างต้นไม่ใช่วิธีที่ดีในการกำหนดฟังก์ชันความยาวเนื่องจากไม่ได้ใช้ ...
เรียกซ้ำแบบหางเรียก
การเรียกซ้ำแบบเรียกหางเป็นแนวคิดที่สำคัญใน Lisp ใช้การเรียกซ้ำบางชนิดเป็นลูปดังนั้นจึงทำให้ call stack เล็ก ล่าม tinylisp ของคุณจะต้องใช้การเรียกซ้ำแบบหางเรียกที่เหมาะสม!
- หากนิพจน์ส่งคืนของฟังก์ชันหรือแมโครที่ผู้ใช้กำหนดเองเป็นการเรียกไปยังฟังก์ชันหรือแมโครที่ผู้ใช้กำหนดเองอื่นล่ามของคุณจะต้องไม่ใช้การสอบถามซ้ำเพื่อประเมินการโทรนั้น แต่จะต้องแทนที่ฟังก์ชั่นปัจจุบันและข้อโต้แย้งด้วยฟังก์ชั่นใหม่และข้อโต้แย้งและห่วงจนกว่าสายการโทรได้รับการแก้ไข
- ถ้านิพจน์ส่งคืนของฟังก์ชันหรือแมโครที่ผู้ใช้กำหนดเป็นการเรียกใช้
i
อย่าประเมินสาขาที่เลือกทันที ตรวจสอบว่าเป็นการเรียกไปยังฟังก์ชันหรือแมโครที่ผู้ใช้กำหนดเองแทนหรือไม่ ถ้าเป็นเช่นนั้นให้สลับฟังก์ชั่นและอาร์กิวเมนต์ดังกล่าวข้างต้นi
นี้นำไปใช้โดยพลการเกิดขึ้นซ้อนกันอย่างลึกซึ้งของ
การเรียกซ้ำแบบหางจะต้องใช้ทั้งสำหรับการเรียกซ้ำโดยตรง (ฟังก์ชั่นเรียกตัวเอง) และการเรียกซ้ำทางอ้อม (ฟังก์ชันa
เรียกใช้ฟังก์ชันb
ที่เรียกใช้ [ฯลฯ ] ซึ่งเรียกใช้ฟังก์ชันa
)
ฟังก์ชันความยาวหางแบบเรียกซ้ำ (พร้อมฟังก์ชันตัวช่วยlen*
):
(d len*
(q (
(list accum)
(i list
(len*
(t list)
(s 1 (s 0 accum))
)
accum
)
))
)
(d len
(q (
(list)
(len* list 0)
))
)
การใช้งานนี้ใช้ได้กับรายการที่มีขนาดใหญ่ตามอำเภอใจโดย จำกัด ด้วยขนาดเต็มจำนวนสูงสุดเท่านั้น
ขอบเขต
พารามิเตอร์ฟังก์ชั่นเป็นตัวแปรท้องถิ่น (จริง ๆ แล้วยังคงอยู่เนื่องจากไม่สามารถแก้ไขได้) พวกเขาอยู่ในขอบเขตในขณะที่เนื้อความของการเรียกฟังก์ชั่นนั้นกำลังถูกดำเนินการและอยู่นอกขอบเขตในระหว่างการโทรที่ลึกและหลังจากที่ฟังก์ชันส่งคืน พวกเขาสามารถ "เงา" ชื่อที่กำหนดไว้ทั่วโลกจึงทำให้ชื่อส่วนกลางไม่สามารถใช้งานได้ชั่วคราว ตัวอย่างเช่นโค้ดต่อไปนี้ส่งคืน 5 ไม่ใช่ 41:
(d x 42)
(d f
(q (
(x)
(s x 1)
))
)
(f 6)
อย่างไรก็ตามรหัสต่อไปนี้จะคืนค่า 41 เนื่องจากx
ที่ระดับการโทร 1 ไม่สามารถเข้าถึงได้จากระดับการโทร 2:
(d x 42)
(d f
(q (
(x)
(g 15)
))
)
(d g
(q (
(y)
(s x 1)
))
)
(f 6)
ชื่อที่อยู่ในขอบเขต ณ เวลาที่กำหนดคือ 1) ชื่อโลคัลของฟังก์ชันการดำเนินการปัจจุบันถ้ามีและ 2) ชื่อโกลบอล
ข้อกำหนดในการส่ง
อินพุตและเอาต์พุต
ล่ามของคุณอาจอ่านโปรแกรมจาก stdin หรือจากไฟล์ที่ระบุผ่าน stdin หรืออาร์กิวเมนต์บรรทัดคำสั่ง หลังจากประเมินแต่ละนิพจน์แล้วควรส่งผลลัพธ์ของนิพจน์นั้นไปยัง stdout ด้วยการขึ้นบรรทัดใหม่
- จำนวนเต็มควรถูกส่งออกในการนำเสนอที่เป็นธรรมชาติที่สุดของภาษาที่ใช้งานของคุณ จำนวนเต็มลบสามารถส่งออกได้โดยมีเครื่องหมายลบนำหน้า
- สัญลักษณ์ควรถูกส่งออกเป็นสตริงโดยไม่มีเครื่องหมายคำพูดหรือ Escape ล้อมรอบ
- รายการควรจะถูกส่งออกด้วยรายการทั้งหมดที่คั่นด้วยช่องว่างและห่อในวงเล็บ ช่องว่างในวงเล็บเป็นตัวเลือก:
(1 2 3)
และ( 1 2 3 )
เป็นรูปแบบที่ยอมรับได้ - เอาท์พุทฟังก์ชั่นในตัวและมาโครเป็นพฤติกรรมที่ไม่ได้กำหนด (การตีความอ้างอิงแสดงเป็น
<built-in function>
.)
อื่น ๆ
ล่ามอ้างอิงรวมถึงสภาพแวดล้อม REPL และความสามารถในการโหลดโมดูล tinylisp จากไฟล์อื่น ๆ ; สิ่งเหล่านี้มีไว้เพื่อความสะดวกและไม่จำเป็นสำหรับความท้าทายนี้
กรณีทดสอบ
กรณีทดสอบจะถูกแบ่งออกเป็นหลายกลุ่มเพื่อให้คุณสามารถทดสอบกรณีที่ง่ายกว่าก่อนที่จะทำงานกับกลุ่มที่ซับซ้อนกว่า อย่างไรก็ตามพวกเขาจะทำงานได้ดีถ้าคุณถ่ายโอนทั้งหมดในไฟล์เดียวเข้าด้วยกัน อย่าลืมที่จะลบส่วนหัวและเอาท์พุทที่คาดไว้ก่อนที่จะเรียกใช้
หากคุณใช้การเรียกซ้ำแบบหางเรียกอย่างถูกต้องกรณีทดสอบสุดท้าย (หลายส่วน) จะกลับมาโดยไม่ทำให้เกิดการล้นสแต็ก การใช้งานอ้างอิงคำนวณในเวลาประมาณหกวินาทีบนแล็ปท็อปของฉัน
-1
ก็ยังสามารถสร้างมูลค่า -1 (s 0 1)
โดยการทำ
F
ไม่สามารถใช้งานได้ในฟังก์ชั่นG
ถ้าการF
โทรG
(เช่นเดียวกับการกำหนดขอบเขตแบบไดนามิก) แต่ก็ยังไม่สามารถใช้ได้ในฟังก์ชั่นH
หากฟังก์ชั่นH
ที่ซ้อนกันที่กำหนดไว้ภายในF
(เช่นเดียวกับการกำหนดขอบเขตคำศัพท์) - ดูกรณีทดสอบ 5 "อาจทำให้เข้าใจผิด