摘要:爲什麼 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 LispLet 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”,包括符號 Tt ,它直接就代表了 “true” 的含義。

當我們需要一次做幾件事時,在其中一個條件分支中,這是我們需要使用 prognblock 的情況之一:

複製代碼

CL-USER> (if(+22)
(progn
(print"hello")
4)
(print"world"))
"hello"
4

然而,我們通常不需要表達式的兩個分支,也就是說,我們並不在乎條件不成立(或成立)時會發生什麼。這是一種非常常見的情況,在 Lisp 中有它的特殊表達式: whenunless

複製代碼

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 中還有更多的條件表達式,用宏來定義自己的條件表達式非常容易(實際上,是關於使用 whenunlesscond 來如何定義的問題),當需要使用特殊表達式時,我們將討論它的實現。

循環

與分支一樣,Lisp 也具有豐富的循環結構,並且在必要時也很容易定義新的結構。這種方法不同於主流語言,主流語言通常只有少量這樣的語句,有時還通過多態性提供擴展機制。它甚至被認爲是一種“美德”,因爲它對初學者而言,不那麼令人困惑。這在一定程度上還是有意義的。不過,在 Lisp 中,通用方法和自定義方法都可以共存並相互補充。然而,定義自定義控件結構的傳統非常強大。爲什麼呢?其中一個理由就是與人類語言相似:實際上, whenunless ,以及 dotimesloop 都是直接來自人類語言中的單詞,或者是來自自然語言表達。我們的母語並沒有那麼原始和枯燥。另一個原因就是因爲你可以。也就是說,在 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 迭代在第一部分(本例中是 iprompt )中定義的多個變量(零或更多),直到滿足第二部分的終止條件(本例中是 (> i 1) ),與 dotimes (以及其他 do-style 宏)一樣,執行它的主體——其餘的表單(本例中是 printterpri ,是打印換行符的簡寫)。 read-line 從標準輸入讀取,直到遇到換行符,並且 1+ 返回 i 的當前值增加 1。

所有 do-style 宏(有很多這樣的宏,既有內置的,也有外部庫提供的: dolistdotreedo-register-groupsdolines 等)都有一個可選的返回值。在 do 中,它遵循終止條件,在本例中,只返回 i 的最終值。

除了 do-style 的迭代外,CL 生態系統中還有一個大不相同的“猛獸”:臭名昭著的 loop 宏。它用途非常廣泛,儘管在語法方面有點不順暢,並且有一些令人驚訝的行爲。但是詳細闡述它已經超出了本書的範疇,特別是因爲在 Peter Seibel 的《 LOOP for Black Belts 》中關於 loop 有一個很不錯的介紹。

許多語言提供了一個通用循環結構,它能夠迭代任意序列、生成器和其他類似的行爲,通常是 foreach 的一些變體。在更詳細地討論序列之後,我們將回到這種結構。

此外還有另一種迭代射血:函數式迭代,基於高階函數( mapreduce 和類似的函數)——我們也將在接下來的章節中對其進行更爲詳細的介紹。

過程和變量

我們已經討論了結構化變成的三大支柱,但其中一個重要的,實際上也是最重要的結構仍然是變量和過程。

如果我告訴你,你可以多次執行相同的計算,但是要改變一些參數的話……好吧,好吧,這個差勁的玩笑。因此,過程是重用計算的最簡單方法,並且過程接受參數,允許將值傳遞到他們的主體中。在 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 許可(創作公用許可協議),供公衆免費閱讀。

原文鏈接:

LISP, THE UNIVERSE AND EVERYTHING

相關文章