Clojure
零基礎(chǔ)
學(xué)習(xí)筆記
數(shù)據(jù)結(jié)構(gòu)
集合
It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures. --- Alan J. Perlis
100 個函數(shù)操作 1 種數(shù)據(jù)結(jié)構(gòu)崖瞭,比 10 個函數(shù)操作 10 種數(shù)據(jù)結(jié)構(gòu)要好 --- Alan J. Perlis
Clojure 提供的基本數(shù)據(jù)結(jié)構(gòu)[1]有
- list
- vector
- map
- set
而上述 4 種數(shù)據(jù)結(jié)構(gòu)我們都稱之為集合(collection)
集合就是把一些東西放在一起沾谓,打個包裹破喻,方便管理职祷。
它們各有特色:
- list 是 Clojure 中最為簡單的數(shù)據(jù)結(jié)構(gòu)
- vector 和 list 相似盖灸,它支持高效的隨機訪問
- map 是一種 “鍵值對” (Key-Value)蚁鳖,在以后的文章中會詳細(xì)介紹這種數(shù)據(jù)結(jié)構(gòu)
- set 是一種不能出現(xiàn)重復(fù)元素的集合
然而由于篇幅限制和本人水平有限
并不能詳盡的說明每一種集合的每一種用法
本文以 list 和 vector 作簡要介紹
更為詳細(xì)的內(nèi)容可以查閱相關(guān) API[2] 文檔
好吧讀完上面的東西你可能想說
什么玩意兒
沒事兒,文字描述總是很枯燥
我們更傾向于使用代碼來向你介紹
我們使用 list
函數(shù)來創(chuàng)建一個 list (列表)
=> (list "hello" "list")
("hello" "list")
如你所見赁炎, list
函數(shù)的返回值就是一個 list醉箕。
列表里的每一個內(nèi)容稱之為元素,而元素被小括號包圍徙垫,這就構(gòu)成了一個列表琅攘。
此例中的這個 list 包含兩個元素 --- 字符串"hello"
和字符串 "list"
。
你也許已經(jīng)注意到了松邪,list 的“樣子”看起來很眼熟坞琴,
沒錯,它的書寫方式和 Clojure 表達(dá)式一樣逗抑,Clojure 語言本身就使用這種數(shù)據(jù)結(jié)構(gòu)來表示語言本身剧辐。
使用自身的數(shù)據(jù)結(jié)構(gòu)來表示自身
這種特征我們給它單獨取一個名字,以表示高端邮府,稱之為 “同像性”[3]
又稱 “代碼即數(shù)據(jù)”
(如果你學(xué)習(xí)過編譯相關(guān)的知識荧关,你就能立刻發(fā)現(xiàn)這種做法的意義
Clojure 和所有的 Lisp 一樣,直接使用語法樹作為語言本身
這也是宏(Macro)功能的基礎(chǔ))
Clojure 還提供了一個語法糖[4] --- 單引號 '
褂傀,它的功能是阻止求值忍啤。
它的非語法糖形式是 quote
。
quote
的返回值是其后的代碼本身。
我們知道同波,Clojure 里的表達(dá)式都會被求值鳄梅,如果我們使用阻止求值 quote
或者它的語法糖 '
, Clojure 就會明白未檩,你想要的不是這個表達(dá)式的值戴尸,而是需要這個表達(dá)式本身(也就是數(shù)據(jù))。
=> (quote ("hello" "list"))
("hello" "list")
=> '("hello" "list")
("hello" "list")
如果不進行阻止求值冤狡,那么由于 “hello” 在括號的第一個位置孙蒙,會被認(rèn)為是一個函數(shù)
然而我們并沒有這個函數(shù),自然也就無法得到值悲雳,此時程序扔出一個錯誤
=> ("hello" "list")
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn
;意為:某個 String(即字符串)無法被作為 IFn(即函數(shù))來使用
說了一大堆挎峦,我們再回到 list
,
因為同像性合瓢,我們可以使用阻止求值來得到一個 list
浑测。也可以使用 list
函數(shù)來得到一個 list。
你可能會發(fā)出這樣的疑問
那么這又有什么屁用呢歪玲?
list 是一些 “元素” 的集合,這些元素可以是任意表達(dá)式掷匠,以先后順序存放在 list 中滥崩。
僅僅是存儲還不夠,我們還可以對這個 list 進行各種操作讹语。
這里介紹幾個常用的操作:
-
first
和second
函數(shù)分別用來取出集合中的第一個或第二個元素
=> (first '("hello" "list"))
hello
=> (second '("hello" "list"))
list
-
rest
函數(shù)用來返回除了first
之后的元素钙皮,并以 list 的形式作為返回
=> (rest '("hello" "list"))
("list")
-
nth
函數(shù)用來取出任意指定位置的元素,表示這個位置的索引值放在第三個參數(shù)的位置
=> (nth '("hello" "list") 0) ;取出第 0 號元素
hello
注意顽决,在大多數(shù)程序語言中短条,索引是以 0 開始的
也就是第一個元素的編號為 0,第二個元素的編號為 1 ...
list 可以按照你的直覺進行嵌套:
這樣就使得你可以創(chuàng)造更為復(fù)雜的結(jié)構(gòu)才菠。
我們來看一下如何操作嵌套結(jié)構(gòu)
=> (first '(("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
("屠龍寶刀" "點擊就送")
=> (second (first '(("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")))
點擊就送
我們來講解一下上面第二句代碼
首先我們使用 '
來得到一個擁有 5 個元素的列表
'(("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
這里我們注意茸时,使用 '
會導(dǎo)致它之后的一個括號中的所有內(nèi)容都被阻止求值
所以在內(nèi)層的列表無需再次使用 '
雖然 list
函數(shù)也可以創(chuàng)建一個列表,但這里如果我們不用 '
赋访,而是直接使用 list
函數(shù)則會出現(xiàn)錯誤
=> (list ("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn
為啥呢可都?因為 list
函數(shù)只是把它收到的參數(shù)的值來構(gòu)建出一個列表。所以執(zhí)行到需要得到 ("屠龍寶刀" "點擊就送")
的值的時候蚓耽,因為"屠龍寶刀"
在括號的第一個位置渠牲,所以把它當(dāng)作函數(shù)進行處理了,就發(fā)生了上面的錯誤步悠。
回頭再來看這個列表签杈,五個元素分別為
0號 ("屠龍寶刀" "點擊就送")
列表也可以作為一個元素(實際上任意表達(dá)式都可以作為元素)
1號 "激光劍"
2號 "無盡之刃"
3號 "傳送槍"
4號 "物理學(xué)圣劍"
在它的外面我們又套了一層函數(shù) first
(first '(("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
我們知道,first
函數(shù)的返回值鼎兽,等于它的參數(shù)的第一個位置所存放的元素答姥,
在這里即為 ("屠龍寶刀" "點擊就送")
铣除。
也就是說,
(first '(("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
等價于
("屠龍寶刀" "點擊就送")
把這個值作為外層的 second
函數(shù)的參數(shù)踢涌,進行簡單替換通孽。
替換前:
(second (first '(("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")))
替換后:
(second '("屠龍寶刀" "點擊就送"))
再執(zhí)行 second
函數(shù),取第二個位置睁壁,結(jié)果為 "點擊就送"
背苦。
(在某些環(huán)境下,字符串顯示出來可能會帶有雙引號)
下面我們來看一下 vector
我們可以使用 vector
函數(shù)來創(chuàng)建一個 vector
=> (vector "hello" "vec")
["hello" "vec"]
注意到 vector 看起和 list 有所不同潘明,它使用中括號(方括號)而不是小括號來包圍元素行剂。
我們也可以直接使用中括號(方括號)來得到一個 vector:
=> ["hello" "vec"]
["hello" "vec"]
(這里并不需要使用阻止求值,中括號是一種用來創(chuàng)建 vector 的特殊形式钳降,Clojure 能正確的處理它厚宰,并不會和執(zhí)行函數(shù)的小括號造成混淆。)
還記得本文第一句來自于第一位圖靈獎得主 Alan J. Perlis 的格言么遂填?
我們剛學(xué)到的可以用于 list 的函數(shù)也可以用于 vector
=> (first ["hello" "vec"])
hello
=> (second ["hello" "vec"])
vec
=> (rest ["hello" "vec"])
("vec") ;雖然這里我們使用 rest 操作的是 vector铲觉,但 rest 仍然返回 list 形式
=> (nth ["hello" "vec"] 1)
vec
同樣可以嵌套
=> (second (first [["屠龍寶刀" "點擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"]))
點擊就送
我們甚至可以在 vector 里嵌套 list,或者在 list 里面嵌套 vector
=> (second (first ['("屠龍寶刀" "點擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"]))
點擊就送
=> (second (first '(["屠龍寶刀" "點擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")))
點擊就送
這多虧了不同的數(shù)據(jù)結(jié)構(gòu)使用了同一套抽象
我們得以用統(tǒng)一的形式(同一函數(shù))來操作不同的數(shù)據(jù)結(jié)構(gòu)吓坚,讓你好似在操作同一種數(shù)據(jù)結(jié)構(gòu)
list 和 vector 有很多共同點撵幽,就如同已經(jīng)向你展示的一樣
但 vector 擁有一些其它特性
- 使用
subvec
函數(shù)來取出指定起止位置的內(nèi)容
后兩個參數(shù)分別表示起止的索引位置(不包括結(jié)束位置元素)
=> (subvec [["屠龍寶刀" "點擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"] 1 3)
["激光劍" "無盡之刃"]
這個例子給出的起止位置為 1 3,不包括結(jié)束位置的元素礁击,也就是從 1 開始取到 2
結(jié)果組成一個 vector 作為返回值
- 使用
assoc
函數(shù)來 “改變” 指定位置的內(nèi)容
這個函數(shù)的后兩個參數(shù)分別是要 “改變” 的元素的位置和 “改變” 后的值盐杂。
返回值是 “改變” 后的結(jié)果。
=> (assoc [["屠龍寶刀" "點擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"] 1 "lightsaber")
[["屠龍寶刀" "點擊就送"]
"lightsaber"
"無盡之刃"
"傳送槍"
"物理學(xué)圣劍"]
注意我們使用了帶引號的 “改變”哆窿,這表示 assoc
函數(shù)實際上并沒有對原來的 vector 做出任何改變链烈。
這個特性會在之后的文章中進行說明。
-
nth
語意可以直接被使用
=> (nth ["hello" "vec"] 1)
vec
=> (["hello" "vec"] 1) ;比上面使用 nth 更方便
vec
其實 ["hello" "vec"]
本身就是個函數(shù)挚躯,所以可以把它放在函數(shù)的位置
這個函數(shù)的功能和 nth
一樣强衡,所以我們可以方便的從 vector 里面取數(shù)據(jù)
你可以把你學(xué)到 first
second
rest
用于 map 和 set
但你不能把 nth
用于 map
原因是 map 沒有實現(xiàn) Indexed,而 nth
只能作用于實現(xiàn)了 Indexed 的集合码荔。
而 first
second
rest
可以作用于實現(xiàn)了 Sequence 的集合食侮,所有的 Clojure 集合都實現(xiàn)了 Sequence。
這些關(guān)于集合的抽象實現(xiàn)內(nèi)容并不需要在現(xiàn)在就掌握目胡,感興趣可以自行查詢锯七。
map 會在之后的章節(jié)中做詳細(xì)介紹,它是 Clojure 中非常實用的一種數(shù)據(jù)結(jié)構(gòu)
如果你在閱讀本文時感到吃力誉己,你可以先把文中的代碼在你的機器上運行一下
觀察運行結(jié)果眉尸,然后試著更改參數(shù),看看返回結(jié)果是否滿足你的猜想
待你基本了解工作效果之后,再次閱讀本文
這種學(xué)習(xí)方式會幫助你更好地理解本文(或者其它程序設(shè)計類教程)
除了本文介紹的方法之外噪猾,操作集合的函數(shù)還有很多
你可以訪問 http://clojuredocs.org/ 查閱 API[2] 的小例子
也可以訪問官方網(wǎng)站 http://clojure.github.io/clojure/ 來直接查閱官方 API 說明
英文苦手們可以訪問中文翻譯項目 http://clojure-api-cn.readthedocs.io/en/latest/