跳到主要內容

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, 不表示執行時不會有語法錯誤。

留言

  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 都要花我不少時間消化啊, 待消化完再來好好回。

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

    回覆刪除

張貼留言

這個網誌中的熱門文章

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

以使用 LevelDB 為例。 抓好並編好相關檔案,編譯方式見第三方函式庫附的說明:$ ls include/ # header files leveldb/ $ ls out-shared/libleveldb.so* # shared library out-shared/libleveldb.so@ out-shared/libleveldb.so.1@ out-shared/libleveldb.so.1.20* 下面的例子用 clang++ 編譯,這裡用到的參數和 g++ 一樣。 問題一:找不到 header$ clang++ sample.cpp sample.cpp:5:10: fatal error: 'leveldb/db.h' file not found #include "leveldb/db.h" ^ 1 error generated. 解法:用 -I 指定 header 位置 問題二:找不到 shared library$ clang++ sample.cpp -I include/ /tmp/sample-2e7dd8.o: In function `main': sample.cpp:(.text+0x1e): undefined reference to `leveldb::Options::Options()' sample.cpp:(.text+0x6f): undefined reference to `leveldb::DB::Open(leveldb::Options const&, std::string const&, leveldb::DB**)' sample.cpp:(.text+0x10c): undefined reference to `leveldb::Status::ToString() const' sample.cpp:(.text+0x7d0): undefined reference to `leveldb::Status::ToString() const' clang: error: linker command failed with exit code 1 (u…

virtualbox 使用 USB 裝置

2012-12-16 更新 現在 (4.x 版) 似乎無需做任何設定, 只要有裝 Oracle VM VirtualBox Extension Pack, 在 VirtualBox 視窗右下角按 USB 的圖示, 再點目標裝置, 即可加入或移除該裝置 同一時間只有 host 或 guest 可擁有該裝置, 所以從 guest OS 移除, 相當於接回 host OS 目前 VirtualBox 只支援 USB 2.0 的插槽, 若偵測不到時, 注意一下是否為這個問題 有時拔拔插插, VirtualBox 會進入奇怪的狀態, 接上去 guest OS 無法連接且跳出 device is busy 的錯誤訊息。試看看拔除該裝置, 重開 guest OS (續上則) 若重開 guest OS 無效, 並且 host OS 已移除該裝置, VirtualBox 的 USB 清單卻仍顯示 "captured", 試看看拔除該裝置, 重開 host OS原文網路上搜一下, 比較多是 Ubuntu 當 host 的解法, 我的情況是 Win7 當 host, Ubuntu 當 guest。 這兩篇說明很詳細《Learn How to Set Up USB and Networking Options in VirtualBox》《幻影千瞳的部落格: VirtualBox 使用筆記(二):使用 USB 裝置》 現在的版本圖形介面很好用了, 不用像第二篇說的那樣用指令操作。這裡記下我的操作步驟: 關掉 guest OS 在 VirtualBox 選單, 選擇 guest OS -> Settings -> USB -> Enable USB 2.0 會出現訊息框, 說明要安裝 Oracle VM VirtualBox Extension Pack。下載後安裝它 host OS 插入 USB 隨身碟 在 VirtualBox 選單, 選擇 guest OS -> Settings -> USB, 點右邊有綠色 "+" 的 USB 頭的圖示, 選擇該 USB 隨身碟, 加入它的 filter 從 host OS 移除 USB 隨身碟 開啟 guest OS 插入 USB 隨身碟, 於是 guest OS 會自動偵測…

熟悉系統工具好處多多

記一下以前很困擾, 現在秒殺的小事。 更新這篇的時候, 忘了函式庫用的 man page 裝在那個 package。以前就會想辦法 google, 運氣好一下會找到, 運氣不好會多找一會兒。 這回我想到新作法:$ strace -e open man 3 printf > /dev/null # 發現是讀 /usr/share/man/man3/printf.3.gz $ dpkg --search /usr/share/man/man3/printf.3.gz # 找到套件名稱 manpages-dev $ aptitude show manpages-dev # 確認描述符合, 收工