隨手扎
【閱讀筆記】Common Lisp相關好文閱讀筆記
我對Common Lisp的喜愛應該不用多說。我不知道他還可以帶給我多少驚喜。
節錄幾個Common Lisp文章的相關敘述。
超凡脫俗的極限 - Common Lisp 文/田春
在文中最後寫:
原文: 超凡脫俗的極限 文/田春 鏈接已失效。
我閱讀位置:https://open343.github.io/Writing/zh-cmn-Hant/Overworldly-Common-Lisp.html
這應該是我第二次看,第一次看應該是在原本連結處。
語法
中序表達式可以徹底避免運算符優先級,例如 C 語言的表達式 1+23 在 Lisp 中將寫成 (+ 1 ( 2 3)) ,其中的 + 和 * 都是普通函數的名稱,和其他用戶定義的函數沒有區別。 值得注意的是,小括號的使用並不是必須,只是 Lisp 讀取器的一種標識,完全可以定製。如果用戶喜歡用中括號甚至後序表達式來描述 Lisp 程序,也是有可能的,相關的方法請查詢 Common Lisp 的 get-macro-character 和 set-macro-character 函數。
高度賦予程式成員自由性
Common Lisp 是唯一的允許程序員控制從源代碼到目標程序的所有方面的編程語言 。典型的 Lisp 代碼的處理分為三個階段:讀取、編譯、加載以及執行,其中每個階段都允許程序員介入。
- 在讀取階段,用戶可以設置特殊的讀取宏,用簡潔的形式讀取用戶自定義的對象;
- 在編譯階段,通過定義宏可以執行任意代碼來生成被編譯器所讀取的代碼;
- 在程序加載階段,附加的代碼有機會被執行,例如全局變量的初始化;
- 而在最終的程序執行階段,Lisp 系統還仍然有機會繼續編譯和加載程序的其餘部分,例如補丁,因為包括 compile 和 load 在內的函數是語言規範的一部分。
在讀取階段有set-macro-character
等讀取宏(read macro,我更喜歡使用原文。宏或巨集都不太能表達其強大)。
在編譯階段有defmacro
、define-compiler-macro
等可以使用。
函數風格程式設計
嚴格來講,Common Lisp 並不是純函數式編程語言,儘管最早的函數式程序是用 Lisp 語言寫的 。Common Lisp 在語言層面支持當今所有主要的編程風格,局部代碼裡甚至可以使用類似 GOTO 語句的語法。社區和主流教材所推薦的 Common Lisp 編程思路是這樣的:
程序應當分層設計,每一層的代碼僅使用下層代碼所提供的接口; 從某一層以上,完全使用純函數式編程風格,下層的代碼仍然可以使用命令式風格; 將面向對象(OO)作為 Common Lisp 類型系統的擴展。
面向對象支持 ── CLOS 和 MOP
Common Lisp 的面向對象支持獨樹一幟。Common Lisp Object System(CLOS)是最後添加到 Common Lisp 語言標準中的特性之一,它和其他主流語言的 OO 系統最大的區別在於並非基於「消息傳遞(message passing)」,而是採用了一種全新的基於「廣義函數(generic function)」的設計。概括地說,在 CLOS 中,一個類(class)僅擁有成員變量(稱為 slot),而成員函數並不從屬於特定的類,而是從屬於同名的廣義函數。
CLOS 的另一項重大特色是對元編程(meta programming)的支持。CLOS 本身是分層設計的;第一層提供最常用的基於宏的 OO 接口,帶有易於理解的語法;
第二層提供了基於函數的接口通向 OO 系統的心臟,適用於開發複雜軟件和編程環境的那一類程序員;
而第三層則提供了用於編寫用戶自己的 OO 語言的必要接口,通過這些接口可以修改對象系統的幾乎所有方面。上層適用相鄰的下層提供的接口來實現的。這種分層的設計建立在一種稱為 Meta-Object Protocol(MOP)的思想之上, 該思想理論上可用於任何編程語言的 OO 實現 。遺憾的是,直到 1994 年 ANSI Common Lisp 標準定案,MOP 也沒能成為語言標準的一部分,上述三層 CLOS 接口只有第一層被定義在語言標準中。不過當今幾乎所有的 Common Lisp 實現都以 The Art of Meta-Object Protocol 一書中所描述的 MOP 接口為基礎實現了全部的 CLOS 接口,具體實現之間的差異很小,並且可以通過第三方的兼容層做到完全一致,在事實上並不影響程序員對它們的使用。
狀況處裡系統
自從 C++ 發明以來,try…catch 風格的異常處理機制開始風靡編程語言設計領域。如果沒有接觸更高級別的異常處理機制,程序員很容易誤以為 try…catch 機制已經是編程語言錯誤處理的最高境界了,但其實 *山外有山,Common Lisp 提供了更強的相關特性。1994 年的 ANSI Common Lisp 標準中增加了一種稱為 Common Lisp Condition System(CLCS)的新特性 * 。在 CLCS 中,程序運行中可能拋出類似異常的範疇統稱為 condition,其本身是一個 CLOS 定義的類。它的子類包括 error、warning 等在內,構成了一個完善的層次體系。
異常/狀況處理系統真的是超級強大,之前寫過一篇:common-lisp-狀況condition處理快速筆記
程序的運行方式
計算機程序從源代碼到目標程序的運行方式可謂多種多樣。
- C 程序借助編譯器轉化成二進制可執行代碼,然後脫離編譯環境獨立運行,儘管有時需要訪問 C 開發平台所提供的運行時庫(runtime library);
- Java 程序同樣需要經過編譯過程,但卻得到平台無關的字節碼,然後借助一個龐大的 JVM 環境執行;
- Perl 是最典型的腳本語言,但 Perl 程序無法編譯,每次需要用 Perl 解釋器來執行;
- Python 和 Ruby 則帶有交互環境,並且支持將程序編譯成可以快速加載的字節碼,但程序運行時仍然是依賴於語言平台。
要說明的是,一種語言的具體實現和運行方式與其語言規範可以是完全無關的。Common Lisp 就是最明顯的例子。當今的主流 Common Lisp 平台可以將 Common Lisp 源代碼編譯成含有原生機器碼(native code)的快速加載文件(fasl 文件),然後多個 fasl 文件按一定順序加載到 Common Lisp 平台所在的進程中,再通過一個入口函數開始執行。
由於這種 image 文件裡總是含有全部的 Common Lisp 語言特性,包括編譯器和交互環境在內,無論用戶 Lisp 代碼是否會使用到它們。這樣帶來的主要問題是 image 文件過大,並且可能不安全。因此部分商業 Common Lisp 平台(包括 LispWorks 和 Allegro Common Lisp 等)在上述程序運行方式的基礎上,還允許導出只含有部分Common Lisp環境的受限 image 文件。大概的思路是,用戶提供一個入口函數,然後 Lisp 系統會分析該函數及其所有調用到的其他函數所涉及到的所有相關函數,然後只將這些用到的部分保留在輸出的 image 文件裡,其他不需要的部分直接丟掉。這個過程說起來簡單,實際上卻非常複雜,並且控制其細節的參數有時多達上百個。這個過程通常被稱為 shake。是否支持 shake 特性基本上是商業 Common Lisp 實現和免費實現之家的分水嶺。
上面描述的只是典型的情況。事實上,不同 Common Lisp 平台上程序的編譯和運行方式區別很大。有些 Common Lisp 平台生成的不是原生機器碼,而是字節碼,甚至允許同一個 fasl 文件中混合使用兩種目標代碼。(CMU Common Lisp)另一種較新的實現可以將 Common Lisp 編譯成 JVM 字節碼然後使用 Java 虛擬機來運行。 (ABCommon Lisp)幾種跟 C 語言關係緊密的 Common Lisp 平台可以先將 Common Lisp 程序轉譯成 C 程序然後在後台使用 C 編譯器直接編譯成可加載的目標文件。(Common LispISP, ECommon Lisp, GCommon Lisp)這種多樣性是其他任何語言裡所沒有過的。
我最近開始是用商業版Common Lisp的免費個人版。
補丁系統
Common Lisp 在語言級別支持任意粒度的補丁。
假設一個 24x7 連續運行的服務程序,已經交付使用了,這時卻在程序的某個函數裡發現了一個 Bug。通過修改該程序的源代碼並且重新編譯出一個新版本的軟件交給用戶,是通常的解決辦法。但假如該服務程序足夠關鍵,以致於不能隨意地中止運行,那麼事情就變得麻煩了。幸運的是,Common Lisp 語言天生提供了對補丁的支持: 只需將修改後的函數單獨放進一個源代碼文件,使用與目標平台相同的 Common Lisp 環境將其編譯成 fasl 文件,然後只把這個 fasl 文件交付給用戶就可以了。正在運行中的 Lisp 應用可以通過某種預先設計好的機制加載這個 fasl 文件(例如定期掃描某個補丁目錄或者通過某種預先設計好的遠程控制協議),然後那個函數的新版本就可以直接投入使用了 !
這種補丁特性對於開發 24x7 的工業強度 Lisp 應用具有極大的意義。目前有一些大型商業 Lisp 軟件正在採用類似的方法來維護其用於關鍵業務的基於 Lisp 的服務端程序。其中最典型的就是 ITA (剛剛被 Google 收購)的 QPX 航空票務系統。
儘管這種方式在許多腳本於也可以使用。以下是簡單的Python範例
主程式:
# main.py
import os
import time
PATCH_PATH = "patch/patch.py"
def hello(name):
print("Hello, " + name)
if __name__ == "__main__":
while True:
if os.path.isfile(PATCH_PATH):
with open(PATCH_PATH) as file:
code = file.read()
exec(code)
#print("code: ", code)
os.remove(PATCH_PATH)
hello("World")
time.sleep(5)
補丁:
# patch/patch.py
print("Loading Patch.....")
def hello(name):
print("Hello123, " + name)
作為一名已有 8 年學習和應用生涯的專業 Common Lisp 程序員(專業的意思是幾乎不使用其他語言編程), 我至今沒能學全這門語言的所有特性 ,更不用說各種豐富的第三方語言擴展了。但這並不妨礙我寫出有用的 Common Lisp 程序,因為沒有哪個程序可以用到該語言的所有特性。
連田春大大都這樣 🐷。
有一種思想認為,各種編程語言大同小異,商業軟件公司的程序員總是可以在幾天的時間裡迅速掌握一門新語言並開使用該語言來維護新接手的軟件。這種思路至少對於 Common Lisp 來說是完全錯誤的,對於像 Haskell、Prolog、甚至 Erlang 這些開拓創新的語言也是不合適的。作為一名以編程為職業的程序員, 時間是如此寶貴,以致於絕對不能把寶貴的時間浪費在不斷地跟進各種草率設計的新語言上,而忽略了對編程語言本質和一般編程方法(算法、數據結構等)的理解。當今所有主流操作系統的底層都是用 C 語言寫成的,作為系統接口的 C 語言是使用所有其他語言的基礎,是必須充分掌握的 ;其他推薦學習的語言是 Prolog 和 Haskell,前者是邏輯型編程語言,後者是純函數型語言,均採用了超越傳統的編程思想,可以極大地提高對編程語言多樣性的認識,並對使用傳統語言有所幫助。
Common Lisp 是最高級的編程語言,並且從目前看 Common Lisp 的這一地位將在較長的未來裡一直保持下去。對於追求卓越,敢於挑戰複雜事物的程序員來說,對 Common Lisp 的學習將是一次不可多得的思維創新之旅,有興趣的話不妨一試。
是阿,我一直都是在看各家語言的設計思想,而非表面的某些特色。
Common Lisp 是最高級的編程語言,所以這些思想與特色也可以很容易的帶入Common Lisp裡頭。
學習資源:
對於 Comomn Lisp 語言規範所涉及的知識,有 4 本教材,推薦按給出的順序閱讀,所有這些教材都可以通過書名在 Google 上查到電子版:
- Common Lisp: An Interactive Approach(作者 Stuart C. Chapiro)
- Common Lisp: The Language, Second Edition(作者 Guy L. Steele Jr.)
- On Lisp: Advanced Techniques for Common Lisp(作者 Paul Graham)
- Practical Common Lisp(作者 Peter Seibel)
另一篇我想記東西後來發現很少,就寫一下標題就好:C程序员驯服Common Lisp