C程序員馴服Common Lisp - 入門 - [語言探索]

 版權聲明:轉載時請以超鏈接形式標明文章原始出處和作者信息及本聲明
http://bigwhite.blogbus.com/logs/158733479.html

毫無疑問,Common Lisp是一門龐大且複雜的語言,學習曲線並不平坦。對於一個從未接觸過函數式語言、交互式語言以及動態類型語言的C程序員來說,學習Common Lisp顯然是一個很大的挑戰。

也許有人會問:"C語言已經無所不能了,爲何還要學習Common Lisp?"在這裏我不想說太多冠冕堂皇的話,至少對我而言,理由有三:
一是好奇,在C語言的世界裏待得久了,總想探出頭來吸幾口新鮮空氣,這次我選擇了Common Lisp;
二是爲了變成一名更好的程序員。爲何學習Common Lisp就能成爲一名更好的程序員呢?這不是我的觀點,而是諸多牛人或大師們(包括Paul GrahamPeter Norvig以及另外一個Peter:Peter Seibel等)的觀點。不過不管你們信不信,反正我是信了。這個觀點的關鍵思想就是一門語言可以影響一個程序員的思維方式。我相信Common Lisp可以給我帶來一種不同於以往的新的編程思維方式,這樣至少比只有一種思維方式要好,不是嗎;
最後,Lisp是一門可編程的編程語言,可以很容易擴展自身並且創造一門新的語言。我無法不動心於如此一門強大的語言。

學習總是需要一些付出的。Jolt大獎得主《Practical Common Lisp》的作者Peter Seibel花了一年的時間放下一切潛心學習Common Lisp並終有所成。我們還有工作,有生活壓力,無法像Seibel那樣瀟灑,但我們依舊可以去學習Common Lisp,循序漸進地學,一步一步來"馴服"Common Lisp這個"猛獸"。"猛獸"被馴服後,才能爲你所用,發揮出異常的威力,不是嗎?我們需要的僅是恆心和足夠的耐心罷了。

"馴服"意味着"學會",何爲學會一門語言?只是知曉語法,看懂代碼還遠遠不夠,那些僅僅叫知道或瞭解或"紙上談兵",還談不上真正地"學會"。古人云:"學以致用",只有在實際中可以靈活自如的使用了,才叫真正的"學會"了。

現在只是開始!這裏我會按照C程序員學習C語言的邏輯展開,爲了更加貼近C程序員的思維模式,我選擇了這種相對平滑的學習方式。也許最初的幾篇會讓你覺得Common Lisp很像一門命令式語言^_^!

言歸正傳!學習一門編程語言之前,最好先弄清楚該語言在當前衆多語言中的位置,瞭解一下它的前世今生,這有助於你對這門語言的認知。不過關於Common Lisp的詳細歷史這裏就不贅述了,在進行下面內容之前,請先閱讀一下維基百科,或是讀讀幾本經典Common Lisp書籍(如《ANSI Common Lisp》、《On Lisp》以及《Practical Common Lisp》等)中對Common Lisp歷史的介紹。

Common Lisp是Lisp語言大家族中的一分子,和Scheme等一樣,它也是一門Lisp方言(Dialect)。與C語言相比,Lisp更加古老,是史上第二古老的編程語言,僅次於Fortran。但Common Lisp比C年輕,它是在上世紀80年代誕生的。與C語言普遍採用的"編輯->編譯->調試/執行"的工作方式不同,Common Lisp更多采用的是類似於Python、Ruby那樣的交互式的解釋器工作模式。你在Common Lisp交互環境中就可以完成上述C語言的所有步驟。這種方式目前看來更易於語言的學習(雖然C語言目前也有解釋器的實現,如Ch,但C程序員似乎更喜歡傳統方式)。

目前市面上Common Lisp的實現有很多種,既有商業收費的,也有開源免費的。商業軟件這裏就不提了,常用的免費開源的主流Common Lisp解釋器包括CLISPSBCL(Steel Bank Common Lisp)和Clozure CL。我個人更喜歡使用CLISP,所以後續有關解釋器方面的內容更多以CLISP爲主。

CLISP支持諸多平臺,你可以很容易得到安裝包並順利的完成安裝,關於這方面內容這裏就不贅述了。打開一個終端(Windows下打開一個命令行窗口),敲入"clisp",回車,你就進入到CLISP提供的Common Lisp頂層環境(Top-Level)當中了(若要進入SBCL,敲入sbcl;若要進入Clozure CL,敲入ccl,以上的前提是這些包的可執行程序路徑已經加入到你的PATH環境變量中了),就像這樣:

$ clisp
... ...
Welcome to GNU CLISP 2.44.1 (2008-02-23) <http://clisp.cons.org/>

Copyright (c) Bruno Haible, Michael Stoll 1992, 1993
Copyright (c) Bruno Haible, Marcus Daniels 1994-1997
Copyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998
Copyright (c) Bruno Haible, Sam Steingold 1999-2000
Copyright (c) Sam Steingold, Bruno Haible 2001-2008

Type :h and hit Enter for context help.

[1]> _

對於所謂的"頂層環境",熟悉Python和Ruby等解釋型語言的朋友並不陌生。它就是一個已經加載了標準Common Lisp包的REPL環境。其中REPL是Read-Eval-Print-Loop的縮寫。說白了,這就是一個Common Lisp代碼的執行環境,你在裏面可以輸入Common Lisp代碼,這些代碼可以被直接執行,執行結果也會立刻展現在你的眼前,或如果遇到錯誤/異常時,你還可以在裏面直接進行代碼調試。當然了"頂層"還有一個範圍(Scope)的概念在裏面,用於區分不同變量和函數的作用域。

我們在CLISP中輸入一些字符串、字符以及數字以及簡單表達式:

[1]> "hello lisp"
"hello lisp"
[2]> #\c
#\c
[3]> 1
1
[4]> (+ 1 2)
3

CLISP對於我們的輸入給予了迴應:對於字符串、字符(注意Common Lisp的字符表示法很特別,以#\作爲前綴,#\c即C語言中的'c')以及數字,CLISP進行了回顯(實際上是對輸入求值後的結果),對於"(+ 1 2)"這個計算1和2之和的表達式,CLISP給出了求值後的結果。

我們繼續輸入一個a:

[5]> a

*** - EVAL: variable A has no value
The following restarts are available:
USE-VALUE      :R1      You may input a value to be used instead of A.
STORE-VALUE    :R2      You may input a new value for A.
ABORT          :R3      Abort main loop
Break 1 [6]>

與前面不同的是,這次CLISP給出了錯誤提示,求值器(evaluator)無法找到a綁定的值,CLISP進入異常處理模式,或稱作調試模式。CLISP給出了三種選擇:我們選擇輸入:R3,可以回到top-level主循環;選擇輸入:R2,則可以爲a賦值。

Break 1 [6]> :R2
New A: 5
5
[7]> a
5

SBCL和Clozure CL與CLISP類似,都會有類似的調試模式,退出調試模式的方法參見各自的提示說明即可。

如果要退出CLISP解釋器,我們可以輸入"(quit)",注意quit兩邊的括號也是命令的一部分;在SBCL中,我們可以輸入(SB-EXT:QUIT)退出;Clozure CL的退出方法與CLISP相同。

Common Lisp源代碼是由一組S-expressions(symbolic expression)構成的。什麼是S-expression呢?這個在Common Lisp書籍中很難找到答案,因爲S-expression是一種組織數據的結構,並不是Lisp獨有的,只是Lisp恰好也採用了這種結構來組織存儲Lisp的代碼和數據罷了。在維基百科上,S-expression有一個遞歸的定義:"S-expression要麼是一個被成爲原子(atoms)的單一的數據對象(data object),要麼是一個S-expressions列表(list)。數字、數組、字符串以及符號都是原子",比如:

[1]> 13
13
[2]> #(1 2 3)
#(1 2 3)
[3]> "hello"
"hello"
[4]> #'length
#

數字'13'、數組'#(1 2 3)'、字符串"hello"以及符號'length'都是原子。

Lisp將代碼和數據都存儲於S-expressions當中,這是Lisp與其他主流語言的最大區別之一。我們在編寫Common Lisp源碼時,需要遵循正確的S-expression格式。前面說過Common Lisp解釋器就是一個READ-EVAL-PRINT-LOOP環境,這個環境主要由一個Reader和一個Evaluator構成。Reader負責讀取源文件中的文本或者我們在提示符後面輸入的文本,檢查文本格式是否符合S-Expression要求,直到所有文本都符合格式要求,這樣解釋器就得到了正確的S-expression:

[1]> (+ 1 2))
3
[2]>
*** - READ from ... >: an object
      cannot start with #\)

通過上面例子可以看出,Reader識別出了不符合S-expression格式的源碼文本。

Reader將文本轉換爲S-expressions後,Evaluator就開始對S-expression進行校驗,校驗其是否符合Lisp Code的規範形式(Lisp Form)。

下面的例子說明了Evaluator的作用:

[1]> (foo 1 2)
*** - EVAL: undefined function FOO

毫無疑問,(foo 1 2)是一個有效的S-expression,其通過Reader這關是沒有問題的。但是當Evaluator對S-expression"(foo 1 2)"進行驗證求值時,卻發現無法找到函數foo的定義,這行源碼不合法。

簡單總結Reader和Evaluator的工作流程就是:"源碼文本"通過Reader轉換爲有效的"S-expressions",後者則由Evaluator轉換成有效"Lisp Form"並求值得出結果。

Common Lisp初學者常常被那滿眼的括號所嚇住,不過事實上括號並沒有那麼"可怕"。括號其實主要是給Common Lisp解釋器(Reader和Evaluator)用的,而不是給程序員看的。現今的代碼編輯器都很智能,基本上可以消除括號在編程過程中給你帶來的影響(要說一點影響沒有也不太可能)。

Common Lisp支持多種註釋形式。在C語言中我們用'//'進行單行註釋(C99標準引入),而Common Lisp的單行註釋符號爲';'。C語言採用'/*...*/'進行多行註釋,Common Lisp使用的是'#|...|#'。Common Lisp還提供了一種大多語言都不具備的註釋方式,那就是將註釋直接寫到緊鄰函數定義的參數列表後面的位置上,這樣通過Common Lisp提供的工具,我們可以輕鬆地提取出該函數的註釋,並生成代碼文檔,比如:

[1]> (defun foo (x) "test comments" (+ x 1))
FOO
[2]> (documentation #'foo t)
"test comments"

由於Common Lisp括號衆多,一個風格良好的Lisp程序需要通過良好風格的代碼縮進來保證,這方面我推薦AI領域大師Peter Norvig若干年前編寫的一篇有關優秀Lisp編程風格的文章《Tutorial on Good Lisp Programming Style》。

很多C程序員可能還是習慣於將代碼寫到文件中。Common Lisp解釋器提供了將你的源文件加載到頂層環境並直接使用其中的定義的方法:
;; foo.lisp
(defun foo (x) "test foo"
   (+ x 1))

[1]> (load "foo.lisp")
;; Loading file foo.lisp ...
;; Loaded file foo.lisp
T
[2]> (foo 5)
6

利用load函數我們可將你的源文件加載到頂層環境中,並在頂層環境裏使用該源文件中定義的函數。

編程語言初學者總喜歡在終端控制檯上看到自己編寫的程序的輸出結果,那樣會產生一種奇妙的成就感,程序員們多陶醉於其中。C程序員最常用的就是printf函數了,Common Lisp中也有與printf等價的函數,它就是format。這裏不是專門講解format函數的,下面僅僅列舉一些常見的例子,這些例子應該可以滿足你在學習語言初期的需求了:

* 輸出整型數
(format t "~d" 1000000) ==> 1000000
(format t "~x" 1000000) ==> f4240
(format t "~o" 1000000) ==> 3641100
(format t "~b" 1000000) ==> 11110100001001000000

上面依次是按十進制、16進制、八進制和二進制輸出。

* 輸出浮點數
(format t "~f" 3.1415) ==> 3.1415

* 輸出字符串
(format t "~a" "hello lisp") ==> hello lisp

* 輸出字符
(format t "~c" #\c) ==> c

* 輸出換行符
以下借用《ANSI Common Lisp》書中的一個例子:
(format nil "Dear ~a, ~% Our records indicate..." "Mr. Malatesta")
==> "Dear Mr. Malatesta,
 Our records indicate..."

format函數的第一個參數表示是否輸出到"標準輸出(*STANDARD-OUTPUT*",如果傳入t,則表示輸出到標準輸出設備上。第二個參數與C中的printf函數的第一個參數類似,是一個格式串,不同的是格式串中的指示符(directive)由printf中的'%'變成了'~'。

爲了讓大家更加直觀地瞭解Common Lisp源代碼到底是什麼樣子的,下面將給出一個Common Lisp的例子程序,這個程序用來計算參數字符串中大寫字母的總個數:

我們先給出一個命令式風格的實現版本:
;; upper-char-counter.lisp
(defun upper-char-counter (str)
  (let ((len (length str)) (result 0))
      (do ((i 0 (+ i 1)))
          ((>= i len) result)
        (if (upper-case-p (char str i)) (setf result (1+ result))))))

即使你不懂Common Lisp語法,你也能大致猜測處理這段代碼的邏輯,基本上與下面C代碼是等價的:
int upper_char_counter(const char *str) {
    int result = 0;
    int len = strlen(str);

    int i = 0;
    while (i < len) {
        if (str[i] >= 'A' && str[i] <= 'Z') {
            result++;
        }
        i++;
    }

    return result;
}
 
下面是一個函數式風格的實現版本:

;; upper-char-counter.lisp
(defun upper-char-counter (str)
   (count-if #'upper-case-p str))

[1]> (load "upper-char-counter.lisp")
;; Loading file upper-char-counter.lisp ...
;; Loaded file upper-char-counter.lisp
T
[2]> (upper-char-counter "a5B6CD!")
3

這個版本的代碼顯然更加簡潔,但理解起來有些難度。函數count-if接受一個函數和一個字符串作爲參數,count-if將函數upper-case-p應用於str中的各個字符上,並將返回true(t)的結果個數累加得到最終返回值。

走到這裏,我想大家應該對Common Lisp有了一個感性的認識了,至少可以編寫一些命令式風格的簡單代碼或複製一些現存的代碼放到頂層環境中執行了。如果真的是這樣,那我的目的就達到了^_^。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章