爲什麼我喜歡Lisp語言(clojure)

這篇文章是我在 Simplificator ――我工作的地方――的一次座談內容的摘錄,座談的題目叫做“爲什麼我喜歡Smalltalk語言和Lisp語言”。在此之前,我曾發佈過一篇叫做“ 爲什麼我喜歡Smalltalk? ”的文章。

Lisp是一種很老的語言。非常的老。Lisp有很多變種,但如今已沒有一種語言叫Lisp的了。事實上,有多少Lisp程序員,就有多少種Lisp。這是因爲,只有當你獨自一人深入荒漠,用樹枝在黃沙上爲自己喜歡的Lisp方言寫解釋器時,你才成爲一名真正的 Lisp程序員 。

目前主要有兩種Lisp語言分支: Common Lisp 和 Scheme ,每一種都有無數種的語言實現。各種Common Lisp實現都大同小異,而各種Scheme實現表現各異,有些看起來非常的不同,但它們的基本規則都相同。這兩種語言都非常有趣,但我卻沒有在實際工作中用過其中的任何一種。這兩種語言中分別在不同的方面讓我苦惱,在所有的Lisp方言中,我最喜歡的是 Clojure語言 。我不想在這個問題上做更多的討論,這是個人喜好,說起來很麻煩。 

Clojure,就像其它種的Lisp語言一樣,有一個REPL(Read Eval Print Loop)環境,你可以在裏面寫代碼,而且能馬上得到運行結果。例如:

1 5
2 ;=> 5
3
4 "Hello world"
5 ;=> "Hello world"

通常,你會看到一個提示符,就像 user> ,但在本文中,我使用的是更實用的顯示風格。這篇文章中的任何REPL代碼你都可以直接拷貝到 Try Clojure 運行。

我們可以像這樣調用一個函數:

1 ( println "Hello World" )
2 ; Hello World
3 ;=> nil

程序打印出“Hello World”,並返回 nil 。我知道,這裏的括弧看起來好像放錯了地方,但這是有原因的,你會發現,他跟Java風格的代碼沒有多少不同:

1 println ( "Hello World" )

這種Clojure在執行任何操作時都要用到括弧:

1 (+ 1 2)
2 ;=> 3

在Clojure中,我們同樣能使用向量(vector):

1 [ 1 2 3 4 ]
2 ;=> [ 1 2 3 4 ]

還有符號(symbol):

1 'symbol
2 ;=> symbol

這裏要用引號('),因爲Symbol跟變量一樣,如果不用引號前綴,Clojure會把它變成它的值。list數據類型也一樣:

1 '(li st)
2 ;=> (li st)

以及嵌套的list:

1 '(l (i s) t)
2 ;=> (l (i s) t)

定義變量和使用變量的方法像這樣:

1 ( def hello-world "Hello world" )
2 ;=> # 'user /hello-world
3
4 hello-world
5 ;=> "Hello world"

我的講解會很快,很多細節問題都會忽略掉,有些我講的東西可能完全是錯誤的。請原諒,我盡力做到最好。

在Clojure中,創建函數的方法是這樣:

1 ( fn [ n ] (* n 2))
2 ;=> #<user$eval1$fn__2 user$eval1$fn__2@175bc6c8>

這顯示的又長又難看的東西是被編譯後的函數被打印出的樣子。不要擔心,你不會經常看到它們。這是個函數,使用 fn 操作符創建,有一個參數 n 。這個參數和2相乘,並當作結果返回。Clojure和其它所有的Lisp語言一樣,函數的最後一個表達式產生的值會被當作返回值返回。

如果你查看一個函數如何被調用:

1 ( println "Hello World" )

你會發現它的形式是,括弧,函數,參數,反括弧。或者用另一種方式描述,這是一個列表序列,序列的第一位是操作符,其餘的都是參數。

讓我們來調用這個函數:

1 (( fn [ n ] (* n 2)) 10)
2 ;=> 20

我在這裏所做的是定義了一個匿名函數,並立即應用它。讓我們來給這個函數起個名字:

1 ( def twice ( fn [ n ] (* n 2)))
2 ;=> # 'user /twice

現在我們通過這個名字來使用它:

1 (twice 32)
2 ;=> 64

正像你看到的,函數就像其它數據一樣被存放到了 變量 裏。因爲有些操作會反覆使用,我們可以使用簡化寫法:

1 ( defn twice [ n ] (* 2 n))
2 ;=> # 'user /twice
3
4 (twice 32)
5 ;=> 64

我們使用if來給這個函數設定一個最大值:

1 ( defn twice [ n ] ( if (> n 50) 100 (* n 2))))

if操作符有三個參數:斷言,當斷言是true時將要執行的語句,當斷言是 false 時將要執行的語句。也許寫成這樣更容易理解:

1 ( defn twice [ n ]
2 ( if (> n 50)
3 100
4 (* n 2)))

非常基礎的東西。讓我們來看一下更有趣的東西。

假設說你想把Lisp語句反着寫。把操作符放到最後,像這樣:

1 (4 5 +)

我們且把這種語言叫做Psil(反着寫的Lisp...我很聰明吧)。很顯然,如果你試圖執行這條語句,它會報錯:

1 (4 5 +)
2 ;=> java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)

Clojure會告訴你 4 不是一個函數(函數是必須是 clojure.lang.IFn 接口的實現)。

我們可以寫一個簡單的函數把Psil轉變成Lisp:

1 ( defn psil [ exp ]
2 (reverse exp))

當我執行它時出現了問題:

1 (psil (4 5 +))
2 ;=> java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)

很明顯,我弄錯了一個地方,因爲在psil被調用之前,Clojure會先去執行它的參數,也就是 (4 5 +) ,於是報錯了。我們可以顯式的把這個參數轉化成list,像這樣:

1 (psil '(4 5 +))
2 ;=> (+ 5 4)

這回它就沒有被執行,但卻反轉了。要想運行它並不困難:

1 ( eval (psil '(4 5 +)))
2 ;=> 9

你開始發現Lisp的強大之處了。事實上,Lisp代碼就是一堆層層嵌套的列表序列,你可以很容易從這些序列數據中產生可以運行的程序。

如果你還沒明白,你可以在你常用的語言中試一下。在數組裏放入2個數和一個加號,通過數組來執行這個運算。你最終得到的很可能是一個被連接的字符串,或是其它怪異的結果。

這種編程方式在Lisp是如此的非常的常見,於是Lisp就提供了叫做 宏(macro) 的可重用的東西來抽象出這種功能。宏是一種函數,它接受未執行的參數,而返回的結果是可執行的Lisp代碼。

讓我們把psil傳化成宏:

1 ( defmacro psil [ exp ]
2 (reverse exp))

唯一不同之處是我們現在使用 defmacro 來替換 defn 。這是一個非常大的改動:

1 (psil (4 5 +))
2 ;=> 9

請注意,雖然參數並不是一個有效的Clojure參數,但程序並沒有報錯。這是因爲參數並沒有被執行,只有當psil處理它時才被執行。 psil 把它的參數按數據看待。如果你聽說過有人說Lisp裏代碼就是數據,這就是我們現在在討論的東西了。數據可以被編輯,產生出其它的程序。這種特徵使你可以在Lisp語言上創建出任何你需要的新型語法語言。

在Clojure裏有一種操作符叫做 macroexpand ,它可以使一個宏跳過可執行部分,這樣你就能看到是什麼樣的代碼將會被執行:

1 (macroexpand '(psil (4 5 +)))
2 ;=> (+ 5 4)

你可以把宏看作一個在編譯期運行的函數。事實上,在Lisp裏,編譯期和運行期是雜混在一起的,你的程序可以在這兩種狀態下來回切換。我們可以讓psil宏變的羅嗦些,讓我們看看代碼是如何運行的,但首先,我要先告訴你 do 這個東西。

do 是一個很簡單的操作符,它接受一批語句,依次運行它們,但這些語句是被整體當作一個表達式,例如:

1 ( do ( println "Hello" ) ( println "world" ))
2 ; Hello
3 ; world
4 ;=> nil

通過使用do,我們可以使宏返回多個表達式,我們能看到更多的東西:

1 ( defmacro psil [ exp ]
2 ( println "compile time" )
3 `( do ( println "run time" )
4 ~(reverse exp)))

新宏會打印出“compile time”,並且返回一個 do 代碼塊,這個代碼塊打印出“run time”,並且反着運行一個表達式。這個反引號 ` 的作用很像引號 ' ,但它的獨特之處是你可以使用 ~ 符號在其內部解除引號。如果你聽不明白,不要擔心,讓我們來運行它一下:

1 (psil (4 5 +))
2 ; compile time
3 ; run time
4 ;=> 9

如預期的結果,編譯期發生在運行期之前。如果我們使用 macroexpand ,或得到更清晰的信息:

1 (macroexpand '(psil (4 5 +)))
2 ; compile time
3 ;=> ( do (clojure.core/ println "run time") (+ 5 4))

可以看出,編譯階段已經發生,得到的是一個將要打印出“run time”的語句,然後會執行 (+ 5 4) 。 println 也被擴展成了它的完整形式,clojure.core/println ,不過你可以忽略這個。然後代碼在運行期被執行。

這個宏的輸出本質上是:

1 ( do ( println "run time" )
2 (+ 5 4))

而在宏裏,它需要被寫成這樣:

1 `( do ( println "run time" )
2 ~(reverse exp))

反引號實際上是產生了一種模板形式的代碼,而波浪號讓其中的某些部分被執行((reverse exp) ),而其餘部分被保留。

對於宏,其實還有更令人驚奇的東西,但現在,它已經很能變戲法了。

這種技術的力量還沒有被完全展現出來。按着" 爲什麼我喜歡Smalltalk? "的思路,我們假設Clojure裏沒有 if 語法,只有 cond 語法。也許在這裏,這並不是一個太好的例子,但這個例子很簡單。

cond 功能跟其它語言裏的 switch 或 case 很相似:

1 ( cond (= x 0) "It's zero"
2 (= x 1) "It's one"
3 :else "It's something else" )

使用 cond ,我們可以直接創建出 my-if 函數:

1 ( defn my- if [ predicate if -true if -false ]
2 ( cond predicate if -true
3 :else if -false))

初看起來似乎好使:

1 (my- if (= 0 0) "equals" "not-equals" )
2 ;=> "equals"
3 (my- if (= 0 1) "equals" "not-equals" )
4 ;=> "not-equals"

但有一個問題。你能發現它嗎? my-if 執行了它所有的參數,所以,如果我們像這樣做,它就不能產生預期的結果了:

1 (my- if (= 0 0) ( println "equals" ) ( println"not-equals" ))
2 ; equals
3 ; not-equals
4 ;=> nil

把 my-if 轉變成宏:

1 ( defmacro my- if [ predicate if -true if -false ]
2 `( cond ~predicate ~ if -true
3 :else ~ if -false))

問題解決了:

1 (my- if (= 0 0) ( println "equals" ) ( println"not-equals" ))
2 ; equals
3 ;=> nil

這只是對宏的強大功能的窺豹一斑。一個非常有趣的案例是,當面向對象編程被髮明出來後(Lisp的出現先於這概念),Lisp程序員想使用這種技術。

C程序員不得不使用他們的編譯器發明出新的語言,C++和Object C。Lisp程序員卻創建了一堆宏,就像defclass, defmethod等。這全都要歸功於宏。變革,在Lisp裏,只是一種進化。

覺得文章有用?立即: 和朋友一起 共學習 共進步!

本文作者:

而且,對文章有任何想法,可:

正在拼命挖掘溝通路線,馬上就通了!
發佈了56 篇原創文章 · 獲贊 17 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章