為什么我如此迷戀Lisp語(yǔ)言?
本文是從 Why I love Lisp 這篇文章翻譯而來(lái)。
這篇文章是我在Simplificator——我工作的地方——的一次座談內(nèi)容的摘錄,座談的題目叫做“為什么我喜歡Smalltalk語(yǔ)言和Lisp語(yǔ)言”。在此之前,我曾發(fā)布過(guò)一篇叫做“ 為什么我喜歡Smalltalk?”的文章。
大漠黃沙 by Guilherme Jófili
Lisp是一種很老的語(yǔ)言。非常的老。Lisp有很多變種,但如今已沒(méi)有一種語(yǔ)言叫Lisp的了。事實(shí)上,有多少Lisp程序員,就有多少種Lisp。這是因?yàn)椋?strong>只有當(dāng)你獨(dú)自一人深入荒漠,用樹(shù)枝在黃沙上為自己喜歡的Lisp方言寫解釋器時(shí),你才成為一名真正的Lisp程序員。
目前主要有兩種Lisp語(yǔ)言分支:Common Lisp 和 Scheme,每一種都有無(wú)數(shù)種的語(yǔ)言實(shí)現(xiàn)。各種Common Lisp實(shí)現(xiàn)都大同小異,而各種Scheme實(shí)現(xiàn)表現(xiàn)各異,有些看起來(lái)非常的不同,但它們的基本規(guī)則都相同。這兩種語(yǔ)言都非常有趣,但我卻沒(méi)有在實(shí)際工作中用過(guò)其中的任何一種。這兩種語(yǔ)言中分別在不同的方面讓我苦惱,在所有的Lisp方言中,我最喜歡的是Clojure語(yǔ)言。我不想在這個(gè)問(wèn)題上做更多的討論,這是個(gè)人喜好,說(shuō)起來(lái)很麻煩。
Clojure,就像其它種的Lisp語(yǔ)言一樣,有一個(gè)REPL(Read Eval Print Loop)環(huán)境,你可以在里面寫代碼,而且能馬上得到運(yùn)行結(jié)果。例如:
- 5
- ;=> 5
- "Hello world"
- ;=> "Hello world"
通常,你會(huì)看到一個(gè)提示符,就像user>,但在本文中,我使用的是更實(shí)用的顯示風(fēng)格。這篇文章中的任何REPL代碼你都可以直接拷貝到Try Clojure運(yùn)行。
我們可以像這樣調(diào)用一個(gè)函數(shù):
- (println "Hello World")
- ; Hello World
- ;=> nil
程序打印出“Hello World”,并返回nil。我知道,這里的括弧看起來(lái)好像放錯(cuò)了地方,但這是有原因的,你會(huì)發(fā)現(xiàn),他跟Java風(fēng)格的代碼沒(méi)有多少不同:
- println("Hello World")
這種Clojure在執(zhí)行任何操作時(shí)都要用到括?。?/p>
- (+ 1 2)
- ;=> 3
在Clojure中,我們同樣能使用向量(vector):
- [1 2 3 4]
- ;=> [1 2 3 4]
還有符號(hào)(symbol):
- 'symbol
- ;=> symbol
這里要用引號(hào)('),因?yàn)镾ymbol跟變量一樣,如果不用引號(hào)前綴,Clojure會(huì)把它變成它的值。list數(shù)據(jù)類型也一樣:
- '(li st)
- ;=> (li st)
以及嵌套的list:
- '(l (i s) t)
- ;=> (l (i s) t)
定義變量和使用變量的方法像這樣:
- (def hello-world "Hello world")
- ;=> #'user/hello-world
- hello-world
- ;=> "Hello world"
我的講解會(huì)很快,很多細(xì)節(jié)問(wèn)題都會(huì)忽略掉,有些我講的東西可能完全是錯(cuò)誤的。請(qǐng)?jiān)?,我盡力做到最好。
在Clojure中,創(chuàng)建函數(shù)的方法是這樣:
- (fn [n] (* n 2))
- ;=> #<user$eval1$fn__2 user$eval1$fn__2@175bc6c8>
這顯示的又長(zhǎng)又難看的東西是被編譯后的函數(shù)被打印出的樣子。不要擔(dān)心,你不會(huì)經(jīng)??吹剿鼈?。這是個(gè)函數(shù),使用fn操作符創(chuàng)建,有一個(gè)參數(shù)n。這個(gè)參數(shù)和2相乘,并當(dāng)作結(jié)果返回。Clojure和其它所有的Lisp語(yǔ)言一樣,函數(shù)的最后一個(gè)表達(dá)式產(chǎn)生的值會(huì)被當(dāng)作返回值返回。
如果你查看一個(gè)函數(shù)如何被調(diào)用:
- (println "Hello World")
你會(huì)發(fā)現(xiàn)它的形式是,括弧,函數(shù),參數(shù),反括弧。或者用另一種方式描述,這是一個(gè)列表序列,序列的第一位是操作符,其余的都是參數(shù)。
讓我們來(lái)調(diào)用這個(gè)函數(shù):
- ((fn [n] (* n 2)) 10)
- ;=> 20
我在這里所做的是定義了一個(gè)匿名函數(shù),并立即應(yīng)用它。讓我們來(lái)給這個(gè)函數(shù)起個(gè)名字:
- (def twice (fn [n] (* n 2)))
- ;=> #'user/twice
現(xiàn)在我們通過(guò)這個(gè)名字來(lái)使用它:
- (twice 32)
- ;=> 64
正像你看到的,函數(shù)就像其它數(shù)據(jù)一樣被存放到了變量里。因?yàn)橛行┎僮鲿?huì)反復(fù)使用,我們可以使用簡(jiǎn)化寫法:
- (defn twice [n] (* 2 n))
- ;=> #'user/twice
- (twice 32)
- ;=> 64
我們使用if來(lái)給這個(gè)函數(shù)設(shè)定一個(gè)最大值:
- (defn twice [n] (if (> n 50) 100 (* n 2))))
if操作符有三個(gè)參數(shù):斷言,當(dāng)斷言是true時(shí)將要執(zhí)行的語(yǔ)句,當(dāng)斷言是 false 時(shí)將要執(zhí)行的語(yǔ)句。也許寫成這樣更容易理解:
- (defn twice [n]
- (if (> n 50)
- 100
- (* n 2)))
非常基礎(chǔ)的東西。讓我們來(lái)看一下更有趣的東西。
假設(shè)說(shuō)你想把Lisp語(yǔ)句反著寫。把操作符放到最后,像這樣:
- (4 5 +)
我們且把這種語(yǔ)言叫做Psil(反著寫的Lisp...我很聰明吧)。很顯然,如果你試圖執(zhí)行這條語(yǔ)句,它會(huì)報(bào)錯(cuò):
- (4 5 +)
- ;=> java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)
Clojure會(huì)告訴你4不是一個(gè)函數(shù)(函數(shù)是必須是clojure.lang.IFn接口的實(shí)現(xiàn))。
我們可以寫一個(gè)簡(jiǎn)單的函數(shù)把Psil轉(zhuǎn)變成Lisp:
- (defn psil [exp]
- (reverse exp))
當(dāng)我執(zhí)行它時(shí)出現(xiàn)了問(wèn)題:
- (psil (4 5 +))
- ;=> java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)
很明顯,我弄錯(cuò)了一個(gè)地方,因?yàn)樵趐sil被調(diào)用之前,Clojure會(huì)先去執(zhí)行它的參數(shù),也就是(4 5 +),于是報(bào)錯(cuò)了。我們可以顯式的把這個(gè)參數(shù)轉(zhuǎn)化成list,像這樣:
- (psil '(4 5 +))
- ;=> (+ 5 4)
這回它就沒(méi)有被執(zhí)行,但卻反轉(zhuǎn)了。要想運(yùn)行它并不困難:
- (eval (psil '(4 5 +)))
- ;=> 9
你開(kāi)始發(fā)現(xiàn)Lisp的強(qiáng)大之處了。事實(shí)上,Lisp代碼就是一堆層層嵌套的列表序列,你可以很容易從這些序列數(shù)據(jù)中產(chǎn)生可以運(yùn)行的程序。
如果你還沒(méi)明白,你可以在你常用的語(yǔ)言中試一下。在數(shù)組里放入2個(gè)數(shù)和一個(gè)加號(hào),通過(guò)數(shù)組來(lái)執(zhí)行這個(gè)運(yùn)算。你最終得到的很可能是一個(gè)被連接的字符串,或是其它怪異的結(jié)果。
這種編程方式在Lisp是如此的非常的常見(jiàn),于是Lisp就提供了叫做宏(macro)的可重用的東西來(lái)抽象出這種功能。宏是一種函數(shù),它接受未執(zhí)行的參數(shù),而返回的結(jié)果是可執(zhí)行的Lisp代碼。
讓我們把psil傳化成宏:
- (defmacro psil [exp]
- (reverse exp))
唯一不同之處是我們現(xiàn)在使用defmacro來(lái)替換defn。這是一個(gè)非常大的改動(dòng):
- (psil (4 5 +))
- ;=> 9
請(qǐng)注意,雖然參數(shù)并不是一個(gè)有效的Clojure參數(shù),但程序并沒(méi)有報(bào)錯(cuò)。這是因?yàn)閰?shù)并沒(méi)有被執(zhí)行,只有當(dāng)psil處理它時(shí)才被執(zhí)行。psil把它的參數(shù)按數(shù)據(jù)看待。如果你聽(tīng)說(shuō)過(guò)有人說(shuō)Lisp里代碼就是數(shù)據(jù),這就是我們現(xiàn)在在討論的東西了。數(shù)據(jù)可以被編輯,產(chǎn)生出其它的程序。這種特征使你可以在Lisp語(yǔ)言上創(chuàng)建出任何你需要的新型語(yǔ)法語(yǔ)言。
在Clojure里有一種操作符叫做macroexpand,它可以使一個(gè)宏跳過(guò)可執(zhí)行部分,這樣你就能看到是什么樣的代碼將會(huì)被執(zhí)行:
- (macroexpand '(psil (4 5 +)))
- ;=> (+ 5 4)
你可以把宏看作一個(gè)在編譯期運(yùn)行的函數(shù)。事實(shí)上,在Lisp里,編譯期和運(yùn)行期是雜混在一起的,你的程序可以在這兩種狀態(tài)下來(lái)回切換。我們可以讓psil宏變的羅嗦些,讓我們看看代碼是如何運(yùn)行的,但首先,我要先告訴你do這個(gè)東西。
do是一個(gè)很簡(jiǎn)單的操作符,它接受一批語(yǔ)句,依次運(yùn)行它們,但這些語(yǔ)句是被整體當(dāng)作一個(gè)表達(dá)式,例如:
- (do (println "Hello") (println "world"))
- ; Hello
- ; world
- ;=> nil
通過(guò)使用do,我們可以使宏返回多個(gè)表達(dá)式,我們能看到更多的東西:
- (defmacro psil [exp]
- (println "compile time")
- `(do (println "run time")
- ~(reverse exp)))
新宏會(huì)打印出“compile time”,并且返回一個(gè)do代碼塊,這個(gè)代碼塊打印出“run time”,并且反著運(yùn)行一個(gè)表達(dá)式。這個(gè)反引號(hào)`的作用很像引號(hào)',但它的獨(dú)特之處是你可以使用~符號(hào)在其內(nèi)部解除引號(hào)。如果你聽(tīng)不明白,不要擔(dān)心,讓我們來(lái)運(yùn)行它一下:
- (psil (4 5 +))
- ; compile time
- ; run time
- ;=> 9
如預(yù)期的結(jié)果,編譯期發(fā)生在運(yùn)行期之前。如果我們使用macroexpand,或得到更清晰的信息:
- (macroexpand '(psil (4 5 +)))
- ; compile time
- ;=> (do (clojure.core/println "run time") (+ 5 4))
可以看出,編譯階段已經(jīng)發(fā)生,得到的是一個(gè)將要打印出“run time”的語(yǔ)句,然后會(huì)執(zhí)行(+ 5 4)。println也被擴(kuò)展成了它的完整形式,clojure.core/println,不過(guò)你可以忽略這個(gè)。然后代碼在運(yùn)行期被執(zhí)行。
這個(gè)宏的輸出本質(zhì)上是:
- (do (println "run time")
- (+ 5 4))
而在宏里,它需要被寫成這樣:
- `(do (println "run time")
- ~(reverse exp))
反引號(hào)實(shí)際上是產(chǎn)生了一種模板形式的代碼,而波浪號(hào)讓其中的某些部分被執(zhí)行((reverse exp)),而其余部分被保留。
對(duì)于宏,其實(shí)還有更令人驚奇的東西,但現(xiàn)在,它已經(jīng)很能變戲法了。
這種技術(shù)的力量還沒(méi)有被完全展現(xiàn)出來(lái)。按著" 為什么我喜歡Smalltalk?"的思路,我們假設(shè)Clojure里沒(méi)有if語(yǔ)法,只有cond語(yǔ)法。也許在這里,這并不是一個(gè)太好的例子,但這個(gè)例子很簡(jiǎn)單。
cond 功能跟其它語(yǔ)言里的switch 或 case 很相似:
- (cond (= x 0) "It's zero"
- (= x 1) "It's one"
- :else "It's something else")
使用 cond,我們可以直接創(chuàng)建出my-if函數(shù):
- (defn my-if [predicate if-true if-false]
- (cond predicate if-true
- :else if-false))
初看起來(lái)似乎好使:
- (my-if (= 0 0) "equals" "not-equals")
- ;=> "equals"
- (my-if (= 0 1) "equals" "not-equals")
- ;=> "not-equals"
但有一個(gè)問(wèn)題。你能發(fā)現(xiàn)它嗎?my-if執(zhí)行了它所有的參數(shù),所以,如果我們像這樣做,它就不能產(chǎn)生預(yù)期的結(jié)果了:
- (my-if (= 0 0) (println "equals") (println "not-equals"))
- ; equals
- ; not-equals
- ;=> nil
把my-if轉(zhuǎn)變成宏:
- (defmacro my-if [predicate if-true if-false]
- `(cond ~predicate ~if-true
- :else ~if-false))
問(wèn)題解決了:
- (my-if (= 0 0) (println "equals") (println "not-equals"))
- ; equals
- ;=> nil
這只是對(duì)宏的強(qiáng)大功能的窺豹一斑。一個(gè)非常有趣的案例是,當(dāng)面向?qū)ο缶幊瘫话l(fā)明出來(lái)后(Lisp的出現(xiàn)先于這概念),Lisp程序員想使用這種技術(shù)。
C程序員不得不使用他們的編譯器發(fā)明出新的語(yǔ)言,C++和Object C。Lisp程序員卻創(chuàng)建了一堆宏,就像defclass, defmethod等。這全都要?dú)w功于宏。變革,在Lisp里,只是一種進(jìn)化。
原文:http://www.aqee.net/why-i-love-lisp/
【編輯推薦】