讀古今文學網 > Java程序員修煉之道 > 10.1 Clojure介紹 >

10.1 Clojure介紹

我們先來看Clojure跟Java在理念上最重要的差別,即對狀態,變量和存儲的不同認識。如圖10-1所示,Java(跟Groovy和Scala一樣)有一個內存和狀態模型,把變量當作保存可變內容的「盒子」(內存位置)。

圖10-1 命令式語言的內存使用

而Clojure認為值才是真正重要的概念。值可以是數字、字符串、向量、映射、集合,或其他任何東西。一旦創建,值就再也不會改變。這一點真的非常重要,所以我們要再重複一次。一旦創建,Clojure的值就不能再變了,因為它們是不可變的。

這就是說命令式語言那種裝著可變內容的盒子模型不是Clojure思考問題的方式。圖10-2是Clojure處理狀態和內存的方式。它在名字和值之間創建了一個關聯關係。

圖10-2 Clojure的內存使用

這就是綁定,通過特殊形式(def)建立。Clojure中的特殊形式相當於Java的關鍵字,但請注意,Clojure中的術語「關鍵字」含義不同,稍後我們會介紹。

(def)的句法是:

(def<名稱> <值>)
  

如果你覺得這個句法看起來有點怪異,不要擔心,這完全是Lisp的普通句法,你很快就會習慣的。現在你可以假裝是在調用下面這樣一個方法,只是括號的位置不太一樣:

def(<名稱>, <值>)
  

接下來我們要在Clojure的交互式環境中寫一個久經考驗的例子,演示一下(def)的用法。

10.1.1 Clojure的Hello World

如果你還沒裝Clojure,請參見附錄D。然後切換到Clojure所在的目錄,運行如下命令:

java -cp clojure.jar clojure.main
  

這個命令會啟動Clojure的REPL環境。在編寫Clojure代碼時,你會在這個交互環境裡花上很多時間。

user=>是Clojure的會話提示符,你可以把這個會話環境當做高級的調試環境,或者命令行工具:

user=> (def hello (fn  \"Hello world\"))
#\'user/hello
user=> (hello)
\"Hello world\"
  

這段代碼一開始先給標識符hello綁定一個值。(def)就是用來建立標識符(Clojure稱之為符號)和值之間的綁定關係的。底層實現的時候,它也會創建一個對像var,用來表示這種綁定關係(和符號的名字)。

那這裡綁定的值是什麼?這個值是:

(fn  \"Hello world\")
  

這是一個函數,在Clojure中也是一個純正的值(因此也是不可變的)。這個函數沒有參數,返回字符串\"Hello world\"

綁定之後,可以用(hello)執行。Clojure運行時會輸出該函數的計算結果,也就是\"Hello world\"

現在,應該錄入這個例子(如果你還沒做),看看它的表現是不是跟我們說的一樣。完成之後,我們就可以繼續探索了。

10.1.2 REPL入門

在REPL中可以輸入Clojure代碼,也可以執行Clojure函數。它是個交互式環境,而且在前面得出的計算結果不會被丟掉。可以用它做探索式編程,我們會在10.5.4節討論這種編程方式,基本就是不斷試驗代碼。用Clojure開發經常都是先在REPL裡把代碼調好,然後用正確的構件搭出越來越大的函數。

馬上看一個例子。先聲明,再次調用def可以改變符號和值的綁定關係,我們在REPL中看一下。代碼中用的實際上是(def)的變體(defn)

user=> (hello)
\"Hello world\"
user=> (defn hello  \"Goodnight Moon\")
#\'user/hello
user=> (hello)
\"Goodnight Moon\"
  

注意,hello最初的綁定關係一直都在,直到被你改掉,這是REPL的一個關鍵特性。這還是狀態,只不過換了個說法,變成了哪個符號綁定到哪個值上,並且這個狀態存在於用戶輸入的不同行間。

Clojure中沒有可變狀態,但有可以改變綁定值的符號。Clojure不是讓「內存盒子」中的內容改變,而是讓符號綁定到不同的不可變值上。換句話說就是在程序的生命期內,var可以指向不同的值。請參見圖10-3。

圖10-3 可以改變的Clojure綁定

注意 可變狀態和不同綁定兩者之間的區別很微妙,但這個概念很重要,一定要掌握。要記住, 可變狀態是指盒子中的內容變了,而重新綁定是指在不同時間指向不同的盒子。

這段代碼中還溜進了另一個Clojure概念,「定義函數」宏(defn)。宏是類Lisp語言的關鍵概念之一,其核心思想是內置結構和普通代碼之間的區別應該盡可能小。

用宏可以創建跟內置語法類似的形式。創建宏是高級話題,但掌握了它之後,你就能製造出非常強大的工具。

這就是說語言真正的原語系統(特殊形式)可以用一種幾乎無法察覺的方式構建起整個語言的核心。宏(defn)就是這種構建的產物。它只是將函數值綁定到符號的相對簡單的方法(當然,要創建合適的var)。

10.1.3 犯了錯誤

如果你犯錯了,會怎麼樣?比如你漏掉了(函數聲明的一部分,表明這個函數沒有參數)。

user=> (defn hello \"Goodnight Moon\")
#\'user/hello
user=> (hello)
java.lang.IllegalArgumentException: Wrong number of args (0) passed to:
user$hello (NO_SOURCE_FILE:0)
  

所有後果只是hello標識符綁定到了一個未知的東西上。你可以在REPL中重新綁定來修復它:

user=> (defn hello  (println \"Dydh da an Nor\")) ; \"Hello World\" in Cornish
#\'user/hello
user=> (hello)
Dydh da an Nor
nil
user=>
  

跟你猜的一樣,上面這段代碼中的分號(;)表示直到行尾的內容都是註釋,(println)是輸出字符串的函數。注意看(println),它跟所有函數一樣,返回了一個值,在函數執行結束後回顯到REPL中。結果值是nil,相當於Java裡的null

10.1.4 學著去愛括號

奇思妙想和幽默感是程序員文化不可或缺的一部分。說Lisp是「很多煩人的傻括號」的縮寫就是個很古老的笑話。其實Lisp是列表處理(List Processing)的縮寫,真相就是這麼平淡無奇。很多Lisp程序員都用這個笑話自嘲,因為它確實戳到了Lisp語法的痛處。

實際上,這個障礙被誇大了。Lisp句法的確特立獨行,但也不像看起來那麼礙手礙腳。另外,Clojure還為減輕入門的障礙做了幾項創新。

我們再看一下Hello World。調用返回「Hello World」的函數寫成:

(hello)
  

用Java寫應該是這樣(假設你已經在某個類裡定義了hello方法):

hello;
  

但Clojure的表達式不是myFunction(someObj),而是(myFunction someObj)。這種寫法叫波蘭表示法,因為它是19世紀的波蘭數學家發明的。

如果你研究過編譯原理,可能想知道這是否和抽像語法樹(AST)之類的概念有關。簡單地說是「有」。可以證明,用波蘭表示法(Lisp程序員通常管它叫s表達式)寫成的Clojure或其他Lisp程序是其簡單直接的AST表示。

你可以認為Lisp程序是直接用AST寫的。Lisp程序的數據結構表示和代碼沒有本質上的差別,所以代碼和數據是完全可以互換的。這也是Clojure的表示法看起來有點奇怪的原因——類Lisp語言用它來模糊內置的原生代碼、用戶代碼和類庫代碼之間的區別。對於Java程序員來說,這股強大力量對他們的引力要遠遠超過稍微有點古怪的語法。

讓我們更深入地學一些Clojure語法,然後用它寫一些真正的程序。