Clojure 學(xué)習(xí)筆記 :2 你好,集合

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è)常用的操作:

  • firstsecond 函數(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/


  1. 數(shù)據(jù)結(jié)構(gòu):簡單來說,就是如何“擺放”值,和如何對(duì)值進(jìn)行操作的一種抽象 ?

  2. Application Programming Interface,即應(yīng)用編程接口。是一種描述函數(shù)如何工作的描述文檔 ? ?

  3. 同像性并不是 Clojure 的專利,其它很多語言也具有同像性,包括所有 Lisp 方言、機(jī)器語言(匯編語言)、Prolog 等 ?

  4. 語法糖:便于程序員書寫的一種簡化語法的 “甜甜的” 東西 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容