2010年2月20日 星期六

Practical Common Lisp - ch3 Practical: A Simple Database

http://gigamonkeys.com/book/practical-a-simple-database.html

由於之前有學過一點 Scheme 和 scripting language, 初讀前半時覺得有些無趣, 沒有那種神兵利器的感受也沒有新鮮感 [*1]。讀到後半看到 macro 時, 開始覺得有趣, 讀完後覺得「嗯......, 還不錯啦」, 直到自己試著用 Python 想寫類似的功能時, 才驚覺「沒可能!! (請用日文發音) 這麼點簡單的小事, Python 竟然做不到, 而 CL 輕易地做到了!!」[*2]


摘要

  • CL 並不是 pure functional language, 可看到像 imperative language 的條列式寫法 (即像 C 那樣一行行寫下去, 程式也一行行地執行)。
  • 函式、變數名稱沒分大小寫。
  • Nil 表示偽值, 其餘值表示真值。CL 有預設參數, 沒傳參數的預設值為 Nil。若要區分沒傳入參數或傳入 Nil, 得在寫函式參數時指明額外的變數用來表示是否有參數傳入。像 Java、Python 同時有 False 和 null (None), 就沒這問題。很難說何者的作法較佳。
  • plist 有點像 associated list, 但聽別人說 CL 沒有 hash table, 應該還是不同東西吧。現階段也只會照範例程式用。
  • CL 的 format 相當於 C 的 printf, 只不過換用另一種外星語表示參數。
  • macro 很威, 見下文。

macro


CL 可以定義函式或 macro, 定義 macro 和定義函式語法差不多, 差別在於傳給 macro 的參數不會被執行 (evaluated), 而是當成資料交給 macro 處理。運算完的結果再當作程式來執行 [*3]。書上的範例如下:

(defmacro backwards (expr) (reverse expr))

執行範例如下:

CL-USER> (backwards ("hello, world" t format))
hello, world
NIL

若 backwards 是一般函式的話, 這個例子會 compile error, 因為 "hello, word" 不是函式。若想看 macro 展開的結果, 可以用 macroexpand-1 (最後那個是數字一):

CL-USER> (macroexpand-1 '(backwards ("hello, world" t format)))
(FORMAT T "hello, world")
T

書上最後的範例是用 macro 做出產生 SQL where 語法的函式。這個函式會產生比對欄位值的函式 (用 Lisp / Python / Ruby 的角度來看, 可當作 filter 的參數)。這和直接寫死的差別在於:
  1. 可以接受不定的參數。
  2. 只會比對給定的參數, 不需執行沒給定的參數。注意, 是「不需執行」, 不是「不用比對」。
舉例來說, 若資料表查詢欄位有 A、B、C 三者, 寫死的話不管查詢時是傳入 A、B、C 還是只傳 A, 程式至少都會做三次判斷 (是否有傳入欄位 X? 若有, 傳入的值是否符合? )。但用 macro 產生程式的話, 要幾個欄位就只會比對幾個欄位 (比方產生一個函式, 它只比對 B 的值, 不檢查有沒有傳入 A、C)。接受不定參數還有辦法用其它方式搞定, 但不執行多餘的程式, 卻是非得動態產生程式碼不可。而 macro 讓這件事變得很容易。

作者給了個很妙的評論: 用 macro 可以寫出更抽象 (也意味著更短) 的程式, 並且還能執行得更快。和 C 的 macro 差異在於, C 的 macro 是用前置處理器展開的, 它不懂語法, 容易產生很難除的錯 (應該不少人誤加分號, 結果出現一大片莫明奇妙的 compile error 訊息吧)。而 CL 的 macro 是由 compiler 展開的, 比較安全且有彈性 [*4]。讓程式處理資料, 比前置處理器展開文字有更多發揮空間。並且, 在編輯期間展開 macro 意味著不會拖慢執行速度。難怪用過的都說神!!


疑問 / 抱怨

  • 太多外星符號, 像是 「'」、「`」、「#'」、「`」、「,@」。也許習慣後就好。
  • 用 macro 會不會很難除錯? 不易維護?

備註

  1. 有寫過 C/C++/Java 的人, 初次寫 scripting language 應該會很驚呀吧。若沒感到驚呀, 表示連皮毛都沒學到, 寫得太 C/C++/Java 了。不然應該能寫得又短又好懂。
  2. 雖然 Python 可以用 exec 在執行期間執行新產生的程式碼, 仍受限於用編輯器寫程式的框架, 不易用程式處理, 挺多用個文字樣板代換關鍵的程式碼。而產生完的程式, 更難做第二次處理。而 Lisp 用 list 表示程式碼成為它的優勢, 程式碼和資料並無顯著區別, 也難怪各家程式語言學不起 macro 這招。Paul Graham 曾對此吐槽, 若其它語言想加入 macro, 大概得用相似於 Lisp 的表示方式, 只不過這樣就不是一個新的程式語言, 而是另一個 Lisp 方言 (dialect) 了。
  3. 原文寫得比較清楚:

    the Lisp compiler passes the arguments, unevaluated, to the macro code, which returns a new Lisp expression that is then evaluated in place of the original macro call. 
  4. 但 CL 是 dynamic typing, 所以.......即使 macro 展開時沒有 compile error, 不表示執行時不會有語法錯誤。

3 則留言:

  1. 1. 我替高中生寫教材時,經過一番掙扎,將 evaluate 譯成『解譯』。例:『Lisp 編譯器會將 macro 的參數,以未經解譯的形式傳給 macro;macro 會傳回一運算式,此新的運算式會取代原本的 macro 呼叫而被解譯。』

    2. 一個語法不特別優雅的 Python macro 實作是 metapython: http://code.google.com/p/metapython/wiki/Tutorial

    3. More about python meta programming: http://us.pycon.org/2010/conference/schedule/event/96/

    回覆刪除
  2. 你每篇丟出的 link 都要花我不少時間消化啊, 待消化完再來好好回。

    你的譯文前後用詞較為一致, 但大概也是懂得人才看得懂, 還是原文最清楚。

    回覆刪除

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...