通過 Lisp 語言理解編程算法:Lisp 速成課程
摘要:爲什麼 Lisp 代碼如此短呢。包含多個 LISP 頂級表單(全局定義)的代碼部分的註釋以。
閱讀完本章節後,你將會對 Lisp 寫出的代碼是什麼樣的有一個直觀的認識。爲什麼 Lisp 代碼如此短呢?就是因爲 Lisp 使用 “自下而上” 的編程方法。你不是在基礎語言上開發,而是在基礎語言上構件一種你自己的語言,然後再用後者開發。你要是不能想象 Lisp 語言的代碼是什麼樣,可以試着想象 XML,想象 XML 中的每個節點都是函數和自變量,而且可以執行。(Lisp 的代碼都是嵌套和遞歸的,編譯後就是一顆解析樹。沒有數據和代碼之分,而且是動態類型語言。) Lisp 在所有語言裏,具有最高的抽象層次,編程能力最強。(這裏的抽象指編程語言本身的抽象,不是對待編程物的抽象。)Lisp 最突出的特點是“ 代碼即數據,數據即代碼 ”。Lisp 構建計算單元的方式尊崇由具體微小入手,像搭積木一般逐步構建規模,這點非常契合人類思維,並且讓代碼的呈現方式也契合這種方式,便於閱讀,賞心悅目。
我預計本書將有兩個讀者羣:
- 希望在算法和編寫高效程序方面取得進步的讀者——這正是本書的主要讀者羣。
- 使用 Lisp 的讀者,無論他們是熟練的還是有抱負的,他們碰巧也對算法深感興趣。
本章節主要針對第一個讀者羣。讀完這個章節之後,你應該能夠理解本書中其餘部分的 Lisp 代碼了。此外,如果你願意的話,你還可以瞭解運行 Lisp 的基本知識並對其進行實驗。
對於 Lisp 用戶來說,你可能有興趣閱讀這一章節,不過,這麼做只是爲了熟悉我在本書中使用這種語言的風格。另外,你還會發現我對評論中多次提到的問題的立場:使用某些第三方擴展是否合理,以及在多大程度上,作者應該謹慎地只堅持使用標準提供的工具。
Lisp 的核心
東西:它們是預先定義的、始終存在的、不可變的。這些都是構件塊,在其上構建所有其他操作符,包括順序塊操作符 block
、條件表達式 if
和無條件跳轉 go
等。如果 oper
是一個特殊操作符的名稱,則執行此運算符的底層代碼,該代碼以自己獨特的方式處理參數。
-
還有普通函數的求值:如果
oper
是函數名,首先,使用相同的求值規則計算所有參數,然後用得到的值調用函數。 -
最後就是宏(macro)求值。宏提供了一種更改特定表單求值規則的方法。如果
oper
爲宏命名,則用它的代碼代替表達式,然後進行求值。宏是 Lisp 中的主要內容,它們用於構建語言的大部分,併爲用戶提供了一種可訪問的方式來擴展 Lisp 語言。但是,它們與本書的主題相互獨立、完全不同的,因此,本書不再詳細討論。但你可以在 On Lisp 、 Let Over Lambda 這樣的書籍中深入研究 Lisp 的宏。
需要注意的是,在 Lisp 中,語句和表達式之間並沒有區別,沒有特殊的關鍵字,沒有操作符優先規則,以及在其他語言中可能會遇到的其他類似的任意東西。一切都是統一的:從某種意義上說,一切都是表達式,它將被求值並返回一些值。
代碼示例
綜上所述,讓我們考慮一個 Lisp 表達的求值示例。下面的示例代碼實現了著名的二分搜索算法(binary search algorithm)(我們將在下一章中詳細討論):
複製代碼
(when(>(lengthvec)0) (let((beg0) (end(lengthvec))) (do() ((=beg end)) (let((mid(floor(+beg end)2))) (if(>(?vec mid) val) (:=beg (1+mid)) (:=end mid)))) (valuesbeg (?vec beg) (=(?vec beg) val))))
它是一種複合表單。其中,所謂的頂級表單是 when
,它是一個宏,用於一個單子句條件表達式:一個只有 trye- 分支的 if
。首先,它對錶達式 (> (length vec) 0)
進行求值,這是一個應用於兩個參數的邏輯操作符 >
的普通函數:得到的結果是變量 vec
的內容長度和一個常數 0
。如果求值返回 true,那就說明 vec
的長度大於 0
,則表單的其餘部分將以相同的方式進行求值。如果沒有異常發生,則求值結果要麼爲 false(在 Lisp 中爲 nil
),要麼爲從最後一個表單返回的 3 個值 (values…)
。而 ?
是通用訪問操作符,它通過不同的方式抽象來按鍵查詢數據結構。在本例中,它從第二個參數的索引處 vec
中檢索項。下面我們將討論這裏提到的其他操作符。
但首先我要談一談 RUTILS
。它是一個第三方庫,爲標準 Lisp 語法及其基本操作符提供了許多擴展。它存在的原因是 Lisp 標準永遠不會改變,而且,正如這個世界上的任何事物一樣,它也有它的缺陷。此外,我們對優雅高效的代碼的理解也隨着時間的推移而不斷發展。然而,Lisp 標準的最大優勢在於,作者從最基本的語法開始,幾乎在所有級別上都採用了多種方法來修改和發展語言,從而抵消了 Lisp 不可變性的問題。這樣一來解決了我們的最終需求,畢竟:我們對改變標準遠不如對改變語言那樣感興趣。因此, RUTILS
是 Lisp 的進化方式之一,其目的是在不損害語言原則的前提下,使 Lisp 的編程更易於理解。因此在本書中,我將使用 RUTILS
中的一些基本擴展,並將根據需要解釋它們。當然,使用第三方庫是個人偏好和品味的問題,可能不會被某些舊版本的 Lisp 所認可,但不必擔心,在你的代碼中,你完全可以輕鬆將它們替換爲你喜歡的替代庫。
REPL
Lisp 程序不僅應該以簡單腳本的一次性方式運行,而且還應該作爲實時系統運行,這些實時系統不僅需要長時間運行,還要經歷數據的更改、代碼的更改。這種與程序交互的一般方式稱爲“讀取 - 求值 - 輸出”循環(Read-Eval-Print-Loop,REPL),字面意思是 Lisp 編譯器 read
一個表單,用上述規則對其進行 eval
,將結果 print
回給用戶,然後 loop
。
REPL 是與 Lisp 程序交互的默認方式,它與 Unix shell 非常相似。當你運行 Lisp 時(例如,通過在 shell 中輸入 sbcl
),你將會進入 REPL。在本書中,我將在所有基於 REPL 的代碼交互之前使用 REPL 提示( CL-USER>
或類似的提示)。下面是一個例子:
複製代碼
CL-USER> (print"Hello world") "Hello world" "Hello world"
好奇的讀者可能會問, "hello world"
爲什麼打印兩次?這證明了在 Lisp 中,一切都是表達式。與大多數其他語言不同, print
語句不僅將其參數打印到控制檯(或其他輸出流),但也會按原樣返回。這在調試時非常方便,因爲你可以在不更改程序流程的情況下,將幾乎任何表單封裝到一個 print
中。
顯然,如果不需要交互的話,那麼只需“讀取 - 求值”部分即可。但是,更重要的是,Lisp 提供了一種方法來自定義流程的每個階段:
-
在
read
階段,可以通過稱爲讀取宏(reader macro)機制引入特殊語法(“syntax sugar”)。 -
普通宏是自定義
eval
階段的一種方法。 -
從概念上來講,
print
階段是最簡單的階段,並且還有一種通過公共 Lisp 對象系統(Common Lisp Object System,CLOS)的print-object
函數定製對象輸出的標準方法。 -
並且,
loop
階段可以被所需的任何程序邏輯所取代。
譯註:syntax sugar,語法糖,由英國計算機科學家 Peter John Landin 發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便進程員使用。通常來說使用語法糖能夠增加進程的可讀性,從而減少進程代碼出錯的機會。
基本表達式
結構化編程範式指出,所有程序都可以用 3 種基本結構表示:順序執行、分支和循環。讓我們看看這些操作符在 Lisp 中是如何表示的。
順序執行
順序執行是最簡單的程序流程。在所有命令式語言中,如果將多個表單放在一行中並對生成的代碼塊進行求值,則會出現這種情況。像這樣:
複製代碼
CL-USER> (print"hello") (+22) "hello" 4
最後一個表達式返回的值將整個序列的值“變暗”。
這裏,REPL- 交互表單形成了隱式順序代碼單元。然而,在許多情況下,我們需要明確界定這些單元這可以通過 block
操作符來完成:
複製代碼
CL-USER> (blocktest (print"hello") (+22)) "hello" 4
這樣的塊有一個名稱(本例中爲 test
)。這允許通過使用操作符 return-from
提前結束執行:
複製代碼
CL-USER> (blocktest (return-fromtest0) (print"hello") (+22)) 0
一個簡短的 reture
用於從名稱爲 nil
的塊中退出(在我們將進一步討論的大多數循環結構中都是隱式的):
複製代碼
CL-USER> (blocknil (return0) (print"hello") (+22)) 0
最後,如果我們甚至不打算過早地從一個塊返回,可以使用不需要名稱的 progn
操作符:
複製代碼
CL-USER> (progn (print"hello") (+22)) "hello" 4
分支
條件表達式計算它們的第一個表單的值,並根據它執行幾個可選代碼路徑之一。基本條件表達式是 if
:
複製代碼
CL-USER> (ifnil (print"hello") (print"world")) "world"
正如我們所見, nil
在 Lisp 中用來表示邏輯的 “false”。所有其他值在邏輯上都被認爲是 “true”,包括符號 T
或 t
,它直接就代表了 “true” 的含義。
當我們需要一次做幾件事時,在其中一個條件分支中,這是我們需要使用 progn
或 block
的情況之一:
複製代碼
CL-USER> (if(+22) (progn (print"hello") 4) (print"world")) "hello" 4
然而,我們通常不需要表達式的兩個分支,也就是說,我們並不在乎條件不成立(或成立)時會發生什麼。這是一種非常常見的情況,在 Lisp 中有它的特殊表達式: when
和 unless
:
複製代碼
CL-USER> (when(+22) (print"hello") 4) "world" 4 CL-USER> (unless(+22) (print"hello") 4) NIL
正如你所看到的,它也很方便,因爲你不必顯式地將順序表單封裝在 progn
中。
另一個標準條件表達式是 cond
,當我們想要連續求值幾個條件時就使用它:
複製代碼
CL-USER> (cond ((typep4'string) (print"hello")) ((>42) (print"world") nil) (t (print"can't get here"))) "world" NIL
如果前面的條件都不起作用(因爲它的條件總是 “true” 的話)那麼 t
情況就是一個全面控制(catch-all),將被觸發。上面的代碼相當於下面的代碼:
複製代碼
(if(typep4'string) (print"hello") (if(>42) (progn (print"world") nil) (print"can't get here")))
在 Lisp 中還有更多的條件表達式,用宏來定義自己的條件表達式非常容易(實際上,是關於使用 when
、 unless
、 cond
來如何定義的問題),當需要使用特殊表達式時,我們將討論它的實現。
循環
與分支一樣,Lisp 也具有豐富的循環結構,並且在必要時也很容易定義新的結構。這種方法不同於主流語言,主流語言通常只有少量這樣的語句,有時還通過多態性提供擴展機制。它甚至被認爲是一種“美德”,因爲它對初學者而言,不那麼令人困惑。這在一定程度上還是有意義的。不過,在 Lisp 中,通用方法和自定義方法都可以共存並相互補充。然而,定義自定義控件結構的傳統非常強大。爲什麼呢?其中一個理由就是與人類語言相似:實際上, when
和 unless
,以及 dotimes
和 loop
都是直接來自人類語言中的單詞,或者是來自自然語言表達。我們的母語並沒有那麼原始和枯燥。另一個原因就是因爲你可以。也就是說,在 Lisp 中定義自定義語法擴展要比其他語言中容易得多,有時簡直讓人無法抗拒。在許多用例中,它們使代碼變得更加簡單明瞭。
無論如何,對於一個完全的初學者來說,實際上,你必須知道與任何其他語言差不多的迭代結構。最簡單的是 dotimes
,它將計數器變量迭代給定次數(從 0 到 (- times 1)
),並在每次迭代中執行主體。它類似於 C 語言中的 for (int i = 0; i < times; i++)
循環。
複製代碼
CL-USER> (dotimes(i3) (printi)) 0 1 2 NIL
儘管返回值可以在循環頭部中指定,但默認情況下,返回值爲 nil
。
另一方面,最通用(和底層)的循環結構是 do
:
複製代碼
CL-USER> (do((i0(1+ i)) (prompt(read-line) (read-line))) ((>i1) i) (print(pairi prompt)) (terpri)) foo (0"foo") bar (1"bar") 2
do
迭代在第一部分(本例中是 i
和 prompt
)中定義的多個變量(零或更多),直到滿足第二部分的終止條件(本例中是 (> i 1)
),與 dotimes
(以及其他 do-style 宏)一樣,執行它的主體——其餘的表單(本例中是 print
和 terpri
,是打印換行符的簡寫)。 read-line
從標準輸入讀取,直到遇到換行符,並且 1+
返回 i
的當前值增加 1。
所有 do-style 宏(有很多這樣的宏,既有內置的,也有外部庫提供的: dolist
、 dotree
、 do-register-groups
、 dolines
等)都有一個可選的返回值。在 do
中,它遵循終止條件,在本例中,只返回 i
的最終值。
除了 do-style 的迭代外,CL 生態系統中還有一個大不相同的“猛獸”:臭名昭著的 loop
宏。它用途非常廣泛,儘管在語法方面有點不順暢,並且有一些令人驚訝的行爲。但是詳細闡述它已經超出了本書的範疇,特別是因爲在 Peter Seibel 的《 LOOP for Black Belts
》中關於 loop
有一個很不錯的介紹。
許多語言提供了一個通用循環結構,它能夠迭代任意序列、生成器和其他類似的行爲,通常是 foreach
的一些變體。在更詳細地討論序列之後,我們將回到這種結構。
此外還有另一種迭代射血:函數式迭代,基於高階函數( map
、 reduce
和類似的函數)——我們也將在接下來的章節中對其進行更爲詳細的介紹。
過程和變量
我們已經討論了結構化變成的三大支柱,但其中一個重要的,實際上也是最重要的結構仍然是變量和過程。
如果我告訴你,你可以多次執行相同的計算,但是要改變一些參數的話……好吧,好吧,這個差勁的玩笑。因此,過程是重用計算的最簡單方法,並且過程接受參數,允許將值傳遞到他們的主體中。在 Lisp 中,過程稱爲 lambda
。可以這樣定義一個: (lambda (x y) (+ x y))
。當使用時,這樣的過程——通常也被稱爲函數,儘管它與我們所認爲的數學函數大不相同,並且在這種情況下,它被稱爲匿名函數,因爲它沒有任何名稱,將產生期輸入的總和:
複製代碼
CL-USER> ((lambda(xy) (+x y))22) 4
通過完整的代碼簽名來引用過程是相當麻煩的,顯而易見的解決方案是爲它們指定名稱。在 Lisp 中,一個常見的方法是通過 defun
宏:
複製代碼
CL-USER> (defunadd2 (xy) (+x y)) ADD2 CL-USER> (add222) 4
過程的參數是變量的例子。變量用於命名存儲單元(memory cells),這些單元的內容被多次使用,並且可能在過程中被更改。他們有不同的用途:
*standard-output*
我們可以沒有變量嗎?從理論上來說,也許可以。至少,編程中有一種所謂的“無點”風格(point-free style),就強烈反對使用變量。但是,就像他們所說,不要在工作中嘗試這個(至少在你完全明白你在做什麼之前)。我們是否可以用常量或者單賦值變量來替換變量,即不能隨時間變化的變量?這種方法是由所謂的純函數語言所提倡的。在某種程度上來說,是這樣。但是,從算法開發的角度來看,它使許多優化變得複雜了,即使沒有完全超越它們,也會令開發者頭疼。
那麼,如何在 Lisp 中定義變量呢?你已經看到了一些變體:過程參數和 let
-bindings。用 Lisp 的話說,這樣的變量叫做局部變量或詞法變量(lexical variable)。這是因爲在整個代碼塊的執行過程中,它們只能在本地訪問,在代碼中定義它們。 let
是引入此類局部變量的一般方法,它是僞裝的 lambda
(“在它上面有一層薄薄的語法糖”):
複製代碼
CL-USER> (let((x2)) (+x x)) 4 CL-USER> ((lambda(x) (+x x)) 2) 4
使用 lambda
,你可以在一個地方創建一個過程,可能的話,也許可以將它分配一個一個變量(本質上就是 defun
所做的),然後在不同的地方多次應用,讓你定義一個過程並立即調用它,這樣就沒有辦法存儲它並在以後再次重新應用。這甚至比匿名函數更加匿名!而且,它還不需要編譯器的額外開銷。但機制是相同的。
通過 let
創建變量稱爲綁定(binding),因爲他們會立即被賦值(綁定)。可以一次綁定多個變量:
複製代碼
CL-USER> (let((x2) (y2)) (+x y)) 4
但是,我們通常需要使用前一個變量的值來定義一行變量和下一個變量。使用 let
很麻煩,因爲需要嵌套(因爲過程參數是獨立分配的):
複製代碼
(let((len(lengthlist))) (let((mid(floorlen2))) (let((left-part(subseqlist0mid)) (right-part(subseqlist mid))) ...)))
爲了簡化這個用例,我們用 let
來演示:
複製代碼
(let*((len(lengthlist)) (mid(floorlen2)) (left-part(subseqlist0mid)) (right-part(subseqlist mid))) ...)
然而,還有許多其他方法可以定義變量:一次綁定多個值;當數據結構(通常是列表)的內容被分配給多個變量時,執行所謂的“析構(destructuring)綁定”,第一個元素分配給第一個變量,第二個元素分配給第二個變量,以此類推;訪問某個結構的槽(slots)等。對於這樣的用例,有來自 RUTILS 的 with
綁定,其工作方式類似於 let
,具有額外的功能。這裏有一個非常簡單的例子:
複製代碼
(with((len(lengthlist)) (midrem (floorlen2)) ;; this group produces a list of 2 sublists ;; that are bound to left-part and right-part ;; and ; character starts a comment in lisp ((left-partright-part) (groupmid list))) ...
在本書的代碼中,你將只看到這兩種綁定結構: let
用於普定綁定和並行綁定,以及 with
用於其餘所有綁定。
正如我們所說的,變量不僅可以被定義,或者它們可以被稱爲“常量“,而且還可以被修改。要改變變量的值,我們將使用來自 RUTILS 的 :=
(它是標準 psetf
宏的縮寫):
複製代碼
CL-USER> (let((x2)) (print(+x x)) (:=x4) (+x x)) 4 8
一般來說,修改是一種危險的夠早,因爲它可能會產生意想不到的“超距作用”效果,當在代碼的某個位置更改變量的值時,會影響使用相同變量的不同部分的執行。然而,這不可能發生在詞法變量上:每個 let
都創建自己的作用域,以保護前面的值不被修改(就像將參數傳遞給過程調用,並在調用中修改它們不會改變調用代碼中那些值一樣):
複製代碼
CL-USER> (let((x2)) (print(+x x)) (let((x4)) (print(+x x))) (print(+x x))) 4 8 4
顯然,當兩個 let
在不同的地方使用同一個變量名時,它們不會相互影響,這兩個變量實際上是完全不同的。
然而,有時在一個地方修改變量,然後在另一個地方查看效果還是有用的。具有這種行爲的變量稱爲全局變量或動態變量(在 Lisp 術語中也稱爲特殊變量)。它們有幾個重要的目的。其中之一是定義需要在任何地方都可訪問的重要配置參數。另一個是引用通用單例對象,如標準流或隨機數生成器的狀態。還有一個是指向某些上下文,這些上下文可以根據特定過程的需要在某些地方進行更高(, *package*
全局變量確定我們在哪個包中操作——前面所有的示例中的 CL-USER
)。全局變量也有更高級的用法。定義全局變量的常用方法是使用 defparameter
,它指定全局變量的初始值:
複製代碼
(defparameter*connection*nil "A default connection object."); this is a docstring describing the variable
在 Lisp 中,全局變量通常在其名稱周圍有所謂的“耳罩”,以提醒用戶他們正在處理的是什麼內容。由於它們的超距作用效果,它並不是最安全的編程語言特性,甚至還有一句“全局變量被認爲是有害的”咒語。但是,Lisp 並不是那種“嬌氣”的語言,它發現了許多特殊變量的用途。順便說一句,它們之所以被稱爲是“特殊的”,是因爲它們有一個特殊的功能,極大地拓寬了它們正常使用的可能性:如果將它們綁定在 let
中,則它們將充當詞法變量,也就是說,即前一個值在離開 let
主體時被保留和恢復:
複製代碼
CL-USER> (defparameter*temp*1) *TEMP* CL-USER> (print*temp*) 1 CL-USER> (progn (let((*temp*2)) (print*temp*) (:=*temp*4) (print*temp*)) *temp*) 2 4 1
Lisp 中的過程是一類對象。這意味着你可以將其分配給變量,以及在運行時檢查和重新定義,因此可以使用它來做許多其他有用的操作。RUTILS 函數
call
將調用作爲參數傳遞給它的過程:
複製代碼
CL-USER> (call'add222) 4 CL-USER> (let((add2(lambda(xy) (+x y)))) (calladd222)) 4
注: call
是標準 funcall
的 RUTILS 縮寫。在 20 世紀 60 年代,從變量中調用函數肯定很有趣,但現在它變得如此普遍,以至於不需要前綴了。
實際上,使用 defun
定義函數也會創建一個全局變量,儘管是在函數名稱空間中。函數、類型、類——所有這些對下你給通常都定義爲全局對象。不過,對於函數,有一種方法可以用 flet
在本地定義它們:
複製代碼
CL-USER> (foo1) ;; ERROR: The function COMMON-LISP-USER::FOO is undefined. CL-USER> (flet((foo(x) (1+ x))) (foo1)) 2 CL-USER> (foo1) ;; ERROR: The function COMMON-LISP-USER::FOO is undefined.
註釋
最後,還有一個語法我們需要只到:如何在代碼中添加註釋。只有失敗者纔不會註釋他們的代碼,在本書中,註釋將會被廣泛使用,貫穿本書,來解釋代碼示例的某些部分。Lisp 中,註釋以 ;
字符開頭,以行位結束。因此,下面的代碼段是一個註釋: ; this is a comment
。還有一種常見的註釋風格,即當前代碼行之後的簡短註釋以單個 ;
開頭,如果某個代碼塊前面有較長的註釋,佔據整行或多行,並以 ;;
開頭。包含多個 LISP 頂級表單(全局定義)的代碼部分的註釋以 ;;;
開頭,也佔用整行。此外,每個全局定義都可以有一個特殊的類似註釋的字符串,稱爲“文檔字符串”(docstring),用於描述其用途和用法,並且可以通過編程查詢。綜上所述,不同的註釋可能是這樣子的:
複製代碼
;;; Some code section (defunthis () "This has a curious docstring." ...) (defunthat () ... ;; this is an interesting block don't you find? (blockinteresting (print"hello"))); it prints hello
入門指南
我強烈建議你嘗試使用本書後面章節中的代碼。並試着改進這些代碼,找出問題,並相處解決方案,測量和跟蹤一切。這不僅能夠幫助你掌握一些 Lisp 技能,而且還能更深入理解所討論的算法和數據結構的描述、他們的缺陷以及極端情況。事實上,做到這一點相當容易。你需要做的就是安裝一些 Lisp(最好是 SBCL 或 CCL),添加 Quicklisp,並在其幫助下添加 RUTILS。
正如我上面所說,使用 Lisp 的通常方法是與其 REPL 進行交互。運行 REPL 相當簡單,在我的 Mint Linux 上,運行以下命令:
複製代碼
$ apt-getinstall sbcl rlwrap ... $ rlwrap sbcl ... * (print"hello world") "hello world" "hello world" *
*
是 Lisp 的原始提示符。它基本上和你在 SLIME 中看到的 CL-USER>
提示符是一樣的。你還可以運行 Lisp 腳本文件: sbcl --script hello.lisp
。如果它只包含一行 (print "hello world")
,我們將會看到“hello world”短語被打印到控制檯上。
這是一個有效的設置,但並不是最方便的設置。一個更高級的環境是在 Emacs 內部運行的 SLIME (類似於 vim 的項目,稱爲 SLIMV)。還有許多其他解決方案:一些 Lisp 實現提供了集成開發環境(IDE),某些 IDE 和編輯器也提供了集成。
進入 REPL 後,你必須發出以下命令:
複製代碼
* (ql:quickload:rutilsx) * (use-package:rutilsx) * (named-readtables:in-readtablerutilsx-readtable)
好了,以上就是你需要知道的 Lisp 知識,已經足以開始了。我們將熟悉其他 Lisp 概念,因爲它們將在本書下一章中用到。但是,你現在就可以準備好讀寫 Lisp 程序了。一開始,它們可能看起來很陌生的感覺,但當你克服了最初的障礙並習慣它們的古怪的前綴表層語法,我保證,你將能夠看懂並欣賞它們的清晰和簡潔。
所以,就像他們在 Lisp 島上說的那樣,快樂去探險吧!
作者介紹:
Vsevolod Dyomkin,Lisp 程序員,居住在烏克蘭基輔。精通烏克蘭語、俄語和英語。目前正在撰寫關於 Lisp 的書籍 Programming Algorithms in Lisp ,該書將使用 CC BY-NC-ND 許可(創作公用許可協議),供公衆免費閱讀。