我們上一節介紹了(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-int
和vect-int
符合這一要求),而(identical?)
會檢查它們是否真的是同一個對象。
你可能也注意到了,符號名稱都沒有用駝峰式大小寫2。這在Clojure中很常見,符號通常都用小寫,單詞之間用連字符連接。
2 駝峰式大小寫(Camel-Case)一詞來自Perl語言中普遍使用的大小寫混合格式,而Larry Wall等人所著的暢銷書Programming Perl: Unmatched power for text processing and scripting(O\'Reilly,2012)的封面圖片正是一匹駱駝。——譯者注
Clojure中的
true
與false
Clojure中有兩個值表示邏輯假:
false
和nil
。其他全是邏輯真。很多動態語言都這樣,但對於Java程序員來說這有點奇怪。
掌握了基本的數據結構和操作符,讓我們把之前見過的特殊形式和函數拼到一起,寫一個稍微長點的Clojure函數吧。