讀古今文學網 > Java程序員修煉之道 > 10.2 尋找Clojure:語法和語義 >

10.2 尋找Clojure:語法和語義

我們上一節介紹了(def)(fn)兩個特殊形式(special form)。這裡還有幾個需要你馬上掌握的特殊形式,它們構成了語言的基礎詞彙表。Clojure中還有大量實用的形式和宏,用得越多,認識會越來越深刻。

Clojure中的函數非常多,托它們的福,你能想到的任務很多都可以用Clojure完成。不要因此而沮喪,你應該感到慶幸。你要干的活大部分都有人替你干了,不該高興嗎?

我們在這一節會討論特殊形式的基本工作集,然後是Clojure的原生數據類型(相當於Java的集合)。之後會接著討論Clojure代碼的自然編寫風格——以函數而不是變量為中心。JVM面向對象的性質在底層還會存在,但Clojure強調函數的那種力量在純粹的面向對像語言中表現得不太明顯。

10.2.1 特殊形式新手營

表10-1給出了一些最常用的Clojure特殊形式。你現在最好快速地把這張表過一遍,然後在10.3節遇到具體例子時再回來看看。

表10-1 Clojure一些基本的特殊形式

特殊形式含義 (def符號> <值?>)把符號綁到值上(如果有的話)。如有必要創建與符號對應的var (fn名稱? [參數*] <表達式>*)返回帶有特定參數的函數值,並把它們應用到表達式上。通常跟(def)相結合,變成形式(defn) (if<test> <then> <else>?)如果test的計算結果為true,計算then並產出其結果。否則計算else並產出其結果,當然,前提是else存在 (let[綁定>*] <表達式>*)給局部名稱分配別名值,並隱式定義一個作用域。使得在let作用域內的所有表達式都能獲得該別名 (do表達式>*)按順序計算表達式的值,並產出最後一個的結果 (quote形式>)照原樣返回形式(不經計算)。它只能接受一個形式參數,其他的參數全都會被忽略 (var符號>)返回與符號對應的var(返回一個Clojure JVM對象,不是值)

這個特殊形式列表不算詳盡,並且其中很多特殊形式都有多種用法。表10-1中只是它們的基本用例,而且都不全面。

現在你對一些特殊形式的基本語法有進一步的瞭解了,讓我們轉去看看Clojure的數據結構吧,也看看它們怎麼操作數據。

10.2.2 列表、向量、映射和集

Clojure中有幾個原生數據類型。用的最多的是列表(list),即單向鏈表。

列表通常都用括號圍起來,因為形式一般也是用圓括號,所以這算是一個輕微的語法障礙。況且括號還用來調用函數。所以初學者經常會犯下面這種錯誤:

1:7 user=> (1 2 3)
java.lang.ClassCastException: java.lang.Integer cannot be cast to
clojure.lang.IFn (repl-1:7)
  

之所以會出錯,是因為Clojure中的值非常靈活,它希望第一個參數是函數值(或綁定到函數值上的符號),把2和3當做這個函數的參數。可在上例中1不是函數值,所以Clojure無法編譯。按我們的說法,這個s表達式是無效的。只有有效的s表達式才能作為Clojure形式。

解決辦法是用(quote)形式,它的縮寫是\'。所以我們可以用兩種方式定義列表:

1:22 user=> \'(1 2 3)
(1 2 3)
1:23 user=> (quote (1 2 3))
(1 2 3)
  

(quote)以一種特殊的方式處理它的參數。具體來說就是它不會計算參數,所以第一個參數不是函數值也沒問題。

Clojure的向量(vector)跟數組類似,實際上,基本上可以把Clojure列表等同於Java的LinkedList,向量等同於ArrayList。向量可以用方括號表示,所以下面這些定義都一樣:

1:4 user=> (vector 1 2 3)
[1 2 3]
1:5 user=> (vec \'(1 2 3))
[1 2 3]
1:6 user=> [1 2 3]
[1 2 3]
  

在前面聲明Hello World和其他函數時,就是用向量來表示函數的參數。注意,(vec)形式以一個列表為參數,並用這個列表創建向量,而(vector)形式以多個獨立符號為參數,並返回包含它們的向量。

函數(nth)有兩個參數:集合和索引。它跟Java中List接口的get方法類似。可以用在向量和列表上,也可以用在Java集合甚至字符串(字符的集合)上,請看下例:

1:7 user=> (nth \'(1 2 3) 1)
2
  

Clojure也支持映射(map,相當於Java的HaspMap),定義很簡單:

{key1 value1 key2 \"value2}
  

從映射裡取值也非常簡單:

user=> (def foo {\"aaa\" \"111\" \"bbb\" \"2222\"})
#\'user/foo
user=> foo
{\"aaa\" \"111\", \"bbb\" \"2222\"}
user=> (foo \"aaa\")
\"111\"
  

Clojure把前面帶冒號的映射鍵稱為「關鍵字」:

1:24 user=> (def martijn {:name \"Martijn Verburg\", :city \"London\", :area \"Highbury\"})
#\'user/martijn
1:25 user=> (:name martijn)
\"Martijn Verburg\"
1:26 user=> (martijn :area)
\"Highbury\"
1:27 user=> :area
:area
1:28 user=> :foo
:foo
  

關於關鍵字,請記住下面這些知識點。

  • Clojure的關鍵字是只有一個參數的函數,其參數必須是映射。

  • 在映射上調用這個函數會返回映射裡與該關鍵字函數對應的值。

  • 關鍵字的使用遵循語法對稱性規則,即(my-map :key)(:key my-map)都是合法的。

  • 關鍵字作為值使用時返回自身。

  • 關鍵字在使用之前無需聲明或def

  • Clojure中的函數也是值,因此可以放在映射裡當鍵用。

  • 可以用逗號(但沒必要)來分隔鍵/值對,因為Clojure會把它們當做空格處理。

  • 除了關鍵字,其他符號也能用在映射裡做鍵,但關鍵字太好用了,所以我們要特別提出來,你應該把它用在自己的代碼中。

除了映射字面值,Clojure還有個(map)函數。但不要上當,它不像(list)(map)函數不會產生映射。而是對集合中的元素輪番應用其參數中的函數,並用返回的新值建立一個新集合(實際上是Clojure序列,請參見10.4節)。

1:27 user=> (def ben {:name \"Ben Evans\", :city \"London\", :area \"Holloway\"})
#\'user/ben
1:28 user=> (def authors [ben martijn])
#\'user/authors
1:29 user=> (map (fn [y] (:name y)) authors)
(\"Ben Evans\" \"Martijn Verburg\")
  

(map)還有別的形式,可以一次處理多個集合,但一次輸入一個集合的形式最常用。

Clojure也支持集(set),跟Java的HashSet很像。它的縮寫形式是:

#{\"apple\" \"pair\" \"peach\"}
  

這些數據結構是構建Clojure程序的基礎。

Java土著可能會感到吃驚,居然一直沒有提到對象。這不是說Clojure不是面向對象的,但它對面向對象的觀點的確和Java不一樣。Java認為世界是由封裝了數據和代碼的靜態數據類型組成的。而Clojure強調函數和形式,儘管這些在底層都是由JVM上的對象實現的。

Clojure和Java在世界觀上的差別最終會體現在代碼裡。要充分理解Clojure的觀點,必須用Clojure寫些程序,並弄明白相比Java的面向對像結構它有哪些優勢。

10.2.3 數學運算、相等和其他操作

Clojure沒有Java裡那種意義上的操作符。所以怎麼才能,比如說,讓兩個數相加呢?在Java裡這很容易:

3 + 4
  

但Clojure沒有操作符,只能用函數:

(add 3 4)
  

這也挺好,但我們可以做得更好。因為Clojure裡沒有操作符,所以我們不用為它們保留任何字符。這就是說Clojure的函數名稱可以更加稀奇古怪,所以我們可以這樣寫1:

(+ 3 4)
  

Clojure函數一般都支持變參(參數數量可變),比如還可以這樣:

(+ 1 2 3)
  

這個運算結果是6。

1 例子中的(+)clojure.core命名空間下的函數,能夠接受0到任意數目的參數,假如沒有參數,則返回0。所以雖然Clojure沒有操作符,但有很多提供了操作符功能的核心函數,所以你大可不必擔心怎麼計算3 * 4,用早已準備好的函數(* 3 4)就行了。——譯者注

Clojure的相等形式(相當於Java裡的equals==)狀況稍微有點複雜。Clojure有兩個跟相等相關的形式:(=)(identical?)。注意它們的名字,這全都是因為Clojure不用為操作符保留字符。另外,(=)也是等號,而不是賦值符號。

下面這段代碼設置了一個列表list-int和一個向量vect-int,並比較它們是否相等:

1:1 user=> (def list-int \'(1 2 3 4))
#\'user/list-int
1:2 user=> (def vect-int (vec list-int))
#\'user/vect-int
1:3 user=> (= vect-int list-int)
true
1:4 user=> (identical? vect-int list-int)
false
  

(=)形式會檢查集合是否由相同的對象以相同的順序組成的( list-intvect-int符合這一要求),而(identical?)會檢查它們是否真的是同一個對象。

你可能也注意到了,符號名稱都沒有用駝峰式大小寫2。這在Clojure中很常見,符號通常都用小寫,單詞之間用連字符連接。

2 駝峰式大小寫(Camel-Case)一詞來自Perl語言中普遍使用的大小寫混合格式,而Larry Wall等人所著的暢銷書Programming Perl: Unmatched power for text processing and scripting(O\'Reilly,2012)的封面圖片正是一匹駱駝。——譯者注

Clojure中的truefalse

Clojure中有兩個值表示邏輯假:falsenil。其他全是邏輯真。很多動態語言都這樣,但對於Java程序員來說這有點奇怪。

掌握了基本的數據結構和操作符,讓我們把之前見過的特殊形式和函數拼到一起,寫一個稍微長點的Clojure函數吧。