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 個(gè)函數(shù)操作 1 種數(shù)據(jù)結(jié)構(gòu),比 10 個(gè)函數(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)
集合就是把一些東西放在一起,打個(gè)包裹,方便管理。
它們各有特色:
- list 是 Clojure 中最為簡單的數(shù)據(jù)結(jié)構(gòu)
- vector 和 list 相似,它支持高效的隨機(jī)訪問
- map 是一種 “鍵值對(duì)” (Key-Value),在以后的文章中會(huì)詳細(xì)介紹這種數(shù)據(jù)結(jié)構(gòu)
- set 是一種不能出現(xiàn)重復(fù)元素的集合
然而由于篇幅限制和本人水平有限
并不能詳盡的說明每一種集合的每一種用法
本文以 list 和 vector 作簡要介紹
更為詳細(xì)的內(nèi)容可以查閱相關(guān) API[2] 文檔
好吧讀完上面的東西你可能想說
什么玩意兒
沒事兒,文字描述總是很枯燥
我們更傾向于使用代碼來向你介紹
我們使用 list 函數(shù)來創(chuàng)建一個(gè) list (列表)
=> (list "hello" "list")
("hello" "list")
如你所見, list 函數(shù)的返回值就是一個(gè) list。
列表里的每一個(gè)內(nèi)容稱之為元素,而元素被小括號(hào)包圍,這就構(gòu)成了一個(gè)列表。
此例中的這個(gè) list 包含兩個(gè)元素 --- 字符串"hello" 和字符串 "list"。
你也許已經(jīng)注意到了,list 的“樣子”看起來很眼熟,
沒錯(cuò),它的書寫方式和 Clojure 表達(dá)式一樣,Clojure 語言本身就使用這種數(shù)據(jù)結(jié)構(gòu)來表示語言本身。
使用自身的數(shù)據(jù)結(jié)構(gòu)來表示自身
這種特征我們給它單獨(dú)取一個(gè)名字,以表示高端,稱之為 “同像性”[3]
又稱 “代碼即數(shù)據(jù)”
(如果你學(xué)習(xí)過編譯相關(guān)的知識(shí),你就能立刻發(fā)現(xiàn)這種做法的意義
Clojure 和所有的 Lisp 一樣,直接使用語法樹作為語言本身
這也是宏(Macro)功能的基礎(chǔ))
Clojure 還提供了一個(gè)語法糖[4] --- 單引號(hào) ' ,它的功能是阻止求值。
它的非語法糖形式是 quote。
quote 的返回值是其后的代碼本身。
我們知道,Clojure 里的表達(dá)式都會(huì)被求值,如果我們使用阻止求值 quote 或者它的語法糖 ', Clojure 就會(huì)明白,你想要的不是這個(gè)表達(dá)式的值,而是需要這個(gè)表達(dá)式本身(也就是數(shù)據(jù))。
=> (quote ("hello" "list"))
("hello" "list")
=> '("hello" "list")
("hello" "list")
如果不進(jìn)行阻止求值,那么由于 “hello” 在括號(hào)的第一個(gè)位置,會(huì)被認(rèn)為是一個(gè)函數(shù)
然而我們并沒有這個(gè)函數(shù),自然也就無法得到值,此時(shí)程序扔出一個(gè)錯(cuò)誤
=> ("hello" "list")
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn
;意為:某個(gè) String(即字符串)無法被作為 IFn(即函數(shù))來使用
說了一大堆,我們?cè)倩氐?list,
因?yàn)橥裥?,我們可以使用阻止求值來得到一個(gè) list。也可以使用 list 函數(shù)來得到一個(gè) list。
你可能會(huì)發(fā)出這樣的疑問
那么這又有什么屁用呢?
list 是一些 “元素” 的集合,這些元素可以是任意表達(dá)式,以先后順序存放在 list 中。
僅僅是存儲(chǔ)還不夠,我們還可以對(duì)這個(gè) list 進(jìn)行各種操作。
這里介紹幾個(gè)常用的操作:
-
first和second函數(shù)分別用來取出集合中的第一個(gè)或第二個(gè)元素
=> (first '("hello" "list"))
hello
=> (second '("hello" "list"))
list
-
rest函數(shù)用來返回除了first之后的元素,并以 list 的形式作為返回
=> (rest '("hello" "list"))
("list")
-
nth函數(shù)用來取出任意指定位置的元素,表示這個(gè)位置的索引值放在第三個(gè)參數(shù)的位置
=> (nth '("hello" "list") 0) ;取出第 0 號(hào)元素
hello
注意,在大多數(shù)程序語言中,索引是以 0 開始的
也就是第一個(gè)元素的編號(hào)為 0,第二個(gè)元素的編號(hào)為 1 ...
list 可以按照你的直覺進(jìn)行嵌套:
這樣就使得你可以創(chuàng)造更為復(fù)雜的結(jié)構(gòu)。
我們來看一下如何操作嵌套結(jié)構(gòu)
=> (first '(("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
("屠龍寶刀" "點(diǎn)擊就送")
=> (second (first '(("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")))
點(diǎn)擊就送
我們來講解一下上面第二句代碼
首先我們使用 ' 來得到一個(gè)擁有 5 個(gè)元素的列表
'(("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
這里我們注意,使用 ' 會(huì)導(dǎo)致它之后的一個(gè)括號(hào)中的所有內(nèi)容都被阻止求值
所以在內(nèi)層的列表無需再次使用 '
雖然 list 函數(shù)也可以創(chuàng)建一個(gè)列表,但這里如果我們不用 ' ,而是直接使用 list 函數(shù)則會(huì)出現(xiàn)錯(cuò)誤
=> (list ("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn
為啥呢?因?yàn)?list 函數(shù)只是把它收到的參數(shù)的值來構(gòu)建出一個(gè)列表。所以執(zhí)行到需要得到 ("屠龍寶刀" "點(diǎn)擊就送") 的值的時(shí)候,因?yàn)?code>"屠龍寶刀" 在括號(hào)的第一個(gè)位置,所以把它當(dāng)作函數(shù)進(jìn)行處理了,就發(fā)生了上面的錯(cuò)誤。
回頭再來看這個(gè)列表,五個(gè)元素分別為
0號(hào) ("屠龍寶刀" "點(diǎn)擊就送")列表也可以作為一個(gè)元素(實(shí)際上任意表達(dá)式都可以作為元素)
1號(hào) "激光劍"
2號(hào) "無盡之刃"
3號(hào) "傳送槍"
4號(hào) "物理學(xué)圣劍"
在它的外面我們又套了一層函數(shù) first
(first '(("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
我們知道,first 函數(shù)的返回值,等于它的參數(shù)的第一個(gè)位置所存放的元素,
在這里即為 ("屠龍寶刀" "點(diǎn)擊就送")。
也就是說,
(first '(("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"))
等價(jià)于
("屠龍寶刀" "點(diǎn)擊就送")
把這個(gè)值作為外層的 second 函數(shù)的參數(shù),進(jìn)行簡單替換。
替換前:
(second (first '(("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")))
替換后:
(second '("屠龍寶刀" "點(diǎn)擊就送"))
再執(zhí)行 second 函數(shù),取第二個(gè)位置,結(jié)果為 "點(diǎn)擊就送"。
(在某些環(huán)境下,字符串顯示出來可能會(huì)帶有雙引號(hào))
下面我們來看一下 vector
我們可以使用 vector 函數(shù)來創(chuàng)建一個(gè) vector
=> (vector "hello" "vec")
["hello" "vec"]
注意到 vector 看起和 list 有所不同,它使用中括號(hào)(方括號(hào))而不是小括號(hào)來包圍元素。
我們也可以直接使用中括號(hào)(方括號(hào))來得到一個(gè) vector:
=> ["hello" "vec"]
["hello" "vec"]
(這里并不需要使用阻止求值,中括號(hào)是一種用來創(chuàng)建 vector 的特殊形式,Clojure 能正確的處理它,并不會(huì)和執(zhí)行函數(shù)的小括號(hào)造成混淆。)
還記得本文第一句來自于第一位圖靈獎(jiǎng)得主 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 [["屠龍寶刀" "點(diǎn)擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"]))
點(diǎn)擊就送
我們甚至可以在 vector 里嵌套 list,或者在 list 里面嵌套 vector
=> (second (first ['("屠龍寶刀" "點(diǎn)擊就送") "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"]))
點(diǎn)擊就送
=> (second (first '(["屠龍寶刀" "點(diǎn)擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍")))
點(diǎn)擊就送
這多虧了不同的數(shù)據(jù)結(jié)構(gòu)使用了同一套抽象
我們得以用統(tǒng)一的形式(同一函數(shù))來操作不同的數(shù)據(jù)結(jié)構(gòu),讓你好似在操作同一種數(shù)據(jù)結(jié)構(gòu)
list 和 vector 有很多共同點(diǎn),就如同已經(jīng)向你展示的一樣
但 vector 擁有一些其它特性
- 使用
subvec函數(shù)來取出指定起止位置的內(nèi)容
后兩個(gè)參數(shù)分別表示起止的索引位置(不包括結(jié)束位置元素)
=> (subvec [["屠龍寶刀" "點(diǎn)擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"] 1 3)
["激光劍" "無盡之刃"]
這個(gè)例子給出的起止位置為 1 3,不包括結(jié)束位置的元素,也就是從 1 開始取到 2
結(jié)果組成一個(gè) vector 作為返回值
- 使用
assoc函數(shù)來 “改變” 指定位置的內(nèi)容
這個(gè)函數(shù)的后兩個(gè)參數(shù)分別是要 “改變” 的元素的位置和 “改變” 后的值。
返回值是 “改變” 后的結(jié)果。
=> (assoc [["屠龍寶刀" "點(diǎn)擊就送"] "激光劍" "無盡之刃" "傳送槍" "物理學(xué)圣劍"] 1 "lightsaber")
[["屠龍寶刀" "點(diǎn)擊就送"]
"lightsaber"
"無盡之刃"
"傳送槍"
"物理學(xué)圣劍"]
注意我們使用了帶引號(hào)的 “改變”,這表示 assoc 函數(shù)實(shí)際上并沒有對(duì)原來的 vector 做出任何改變。
這個(gè)特性會(huì)在之后的文章中進(jìn)行說明。
-
nth語意可以直接被使用
=> (nth ["hello" "vec"] 1)
vec
=> (["hello" "vec"] 1) ;比上面使用 nth 更方便
vec
其實(shí) ["hello" "vec"] 本身就是個(gè)函數(shù),所以可以把它放在函數(shù)的位置
這個(gè)函數(shù)的功能和 nth 一樣,所以我們可以方便的從 vector 里面取數(shù)據(jù)
你可以把你學(xué)到 first second rest 用于 map 和 set
但你不能把 nth 用于 map
原因是 map 沒有實(shí)現(xiàn) Indexed,而 nth 只能作用于實(shí)現(xiàn)了 Indexed 的集合。
而 first second rest 可以作用于實(shí)現(xiàn)了 Sequence 的集合,所有的 Clojure 集合都實(shí)現(xiàn)了 Sequence。
這些關(guān)于集合的抽象實(shí)現(xiàn)內(nèi)容并不需要在現(xiàn)在就掌握,感興趣可以自行查詢。
map 會(huì)在之后的章節(jié)中做詳細(xì)介紹,它是 Clojure 中非常實(shí)用的一種數(shù)據(jù)結(jié)構(gòu)
如果你在閱讀本文時(shí)感到吃力,你可以先把文中的代碼在你的機(jī)器上運(yùn)行一下
觀察運(yùn)行結(jié)果,然后試著更改參數(shù),看看返回結(jié)果是否滿足你的猜想
待你基本了解工作效果之后,再次閱讀本文
這種學(xué)習(xí)方式會(huì)幫助你更好地理解本文(或者其它程序設(shè)計(jì)類教程)
除了本文介紹的方法之外,操作集合的函數(shù)還有很多
你可以訪問 http://clojuredocs.org/ 查閱 API[2] 的小例子
也可以訪問官方網(wǎng)站 http://clojure.github.io/clojure/ 來直接查閱官方 API 說明
英文苦手們可以訪問中文翻譯項(xiàng)目 http://clojure-api-cn.readthedocs.io/en/latest/