Clojure 零基礎(chǔ) 學(xué)習(xí)筆記 綁定 解構(gòu)
是時(shí)候給我們的值取個(gè)名字了!
綁定
在之前的學(xué)習(xí)中,我們學(xué)會(huì)了如何使用簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu) --- list 和 vector
但是每次使用的時(shí)候,我們都需要重新寫一遍我們的元素內(nèi)容
使用起來(lái)非常麻煩
于是 Clojure 提供了“取名”的功能:def 函數(shù) (即 define,定義)
def 函數(shù)的第一個(gè)參數(shù)是你想給你的值取的名字,第二個(gè)參數(shù)是你的值(表達(dá)式)。
=> (def my-items ["短劍" "火把" "汽油"])
#'user/my-items
這樣以來(lái)我們就可以更為方便的使用我們的 vector 了
=> (first my-items)
短劍
(注意,def 所定義的內(nèi)容不是變量,會(huì)在之后的文章中討論這一問(wèn)題)
我們觀察它的返回值 #'user/my-items 它表示我們成功地在 user 空間中創(chuàng)建了一個(gè) my-items
user 空間是 REPL 啟動(dòng)后默認(rèn)的命名空間(namespace),它表示我們的 my-items 只在 user 中才能使用
(命名空間的作用和用法,日后再說(shuō)…現(xiàn)在你可以把它理解為一個(gè)文件夾,我們是在 user 這個(gè)“文件夾”下面新建了一個(gè) my-items,如果你去另外的文件夾里面訪問(wèn),即使那個(gè)文件夾也有一個(gè)叫 my-items 的東西,那它也不是我們這個(gè) my-items)
這里我們簡(jiǎn)單提一下 Clojure 的命名規(guī)范
我們使用全小寫字母和減號(hào)間隔來(lái)表示 Clojure 里的一個(gè)值的名稱
如:a-beautiful-world items lovely-girl
使用 def 函數(shù)定義一個(gè)值,這個(gè)值在整個(gè) user 空間都可以被訪問(wèn)
下面我們介紹的 let 函數(shù),它被稱為“局部綁定”
(在其它文章中也稱之為“本地綁定”、“本地值”、“l(fā)et 綁定”。指的都是我們這里介紹的內(nèi)容)
之所以叫它“局部綁定”,是因?yàn)槭褂盟鼇?lái)命名的名稱,只在 let 的括號(hào)范圍內(nèi)才能使用
=> (let [my-items ["短劍" "火把" "汽油"]]
(println my-items))
[短劍 火把 汽油]
nil
我們?cè)诘诙偷谌齻€(gè)參數(shù)之間換行,這并不影響程序的執(zhí)行,無(wú)論你是否換行
但這增加了程序的可讀性[1]
養(yǎng)成良好的編程風(fēng)格,適當(dāng)?shù)目s進(jìn)和換行會(huì)增加程序的可讀性
let 函數(shù)的使用比我們之前所學(xué)的函數(shù)要復(fù)雜一點(diǎn)
首先,它的第一個(gè)參數(shù)是一個(gè) vector (使用中括號(hào)包圍),中括號(hào)里元素個(gè)數(shù)必須為偶數(shù),
因?yàn)橹欣ㄌ?hào)里每一對(duì)元素表示 “給值取的名字” - “值”(key-value),
也就是說(shuō),我們可以一次性地給多個(gè)值取名字然后,它之后的參數(shù)是多個(gè)表達(dá)式,
表達(dá)式會(huì)被 Clojure 依次執(zhí)行最后,
let函數(shù)的返回值等于它最后執(zhí)行的表達(dá)式的值
我們來(lái)觀察更多的例子
=> (let [my-items ["短劍" "火把" "汽油"] my-coins [128]]
(print my-coins)
my-items)
[128]
["短劍" "火把" "汽油"]
=> (let [my-items ["短劍" "火把" "汽油"] my-coins [128]]
my-items
my-coins
(print my-coins))
[128]
nil
=> (let [my-items ["短劍" "火把" "汽油"] my-coins [128]]
my-items
my-coins
(print my-coins))
(print my-coins) ;嘗試在 let 的括號(hào)外訪問(wèn) my-items
[128]
nil
CompilerException java.lang.RuntimeException: Unable to resolve symbol: my-coins in this context
- 第一段代碼展示了如何使用
let來(lái)一次性地定義兩個(gè)值 ---my-items和my-coins
然后在let的括號(hào)范圍內(nèi),我們依次執(zhí)行了兩句表達(dá)式,而且我們?cè)诒磉_(dá)式之間使用換行以增加可讀性
第一句表達(dá)式輸出my-coins的值
第二句表達(dá)式直接使用my-items,沒有使用括號(hào)包圍,這表示我們直接使用它的值,而不是把它作為函數(shù)
由于let函數(shù)規(guī)定最后被執(zhí)行的表達(dá)式的值為let函數(shù)的返回值,所以此例中let函數(shù)的返回值為my-items的值
而第二段代碼與第一段不同的是,它最后才執(zhí)行了
print函數(shù)
我們可以看到,print函數(shù)的副作用效果出現(xiàn) ---my-coins的值[128]被打印出來(lái),
但是由于print函數(shù)的返回值始終為nil
所以此例中let函數(shù)的返回值為最后執(zhí)行的表達(dá)式的值 --- 即print函數(shù)的值,雖然在它之前我們對(duì)my-items和my-coins的值進(jìn)行的訪問(wèn),但由于沒有執(zhí)行打印到屏幕上的操作,我們無(wú)法觀察到它們的值在第三段代碼中,我們嘗試在
let函數(shù)的括號(hào)外訪問(wèn)my-coins,結(jié)果可想而知,錯(cuò)誤信息表示:Unable to resolve symbol: my-coins(無(wú)法理解符號(hào)my-coins)
因?yàn)?let函數(shù)給值取的名字的有效性只在它的“勢(shì)力范圍”之內(nèi),即只能在它前后括號(hào)的范圍內(nèi)使用
很多函數(shù)隱式地使用了 let,在今后的 “定義屬于你自己的函數(shù)” 章節(jié)中,這里所學(xué)習(xí)到的有關(guān) let 的知識(shí)就能夠派上更大的用場(chǎng)了
解構(gòu)
在前一個(gè)章節(jié)中,我們學(xué)習(xí)了如何訪問(wèn)一個(gè)集合中的元素
但如果每次都這樣使用,顯得繁瑣而無(wú)聊
=> (def my-items ["短劍" "火把" "汽油"])
#'user/my-items
=> (first my-items)
短劍
=> (rest my-items)
("火把" "汽油") ;復(fù)習(xí)一下,rest 函數(shù)返回除 first 之外剩余元素的 list 形式
尤其是我們想給一個(gè)集合里面的元素都取一個(gè)新名字時(shí)
=>(def first-item (first my-items))
#'user/first-item
=>(def rest-item (rest my-items))
#'user/rest-item
或者在訪問(wèn)一個(gè)多層嵌套的集合時(shí)
=> (def my-coins 256)
#'user/my-coins
=> (def my-bag [my-items my-coin])
#'user/my-bag
=> (nth (first my-bag) 2)
汽油
可以想象如果一個(gè)集合里面的元素非常多,或者嵌套層數(shù)非常多的時(shí)候,這種方式效率十分低下
不過(guò),當(dāng)你的嵌套層數(shù)非常多的時(shí)候,就該反思一下你的設(shè)計(jì)了
(可能寫出來(lái)的代碼你自己也讀不懂)
幸虧 let 函數(shù)給我們提供了一個(gè)誘人的訪問(wèn)集合元素的方式
=> (def my-bag [["短劍" "火把" "汽油"] 256])
#'user/my-bag
=> (let [[items coins] my-bag]
(println "你所擁有的裝備:" items) ;復(fù)習(xí)一下,print 函數(shù)家族可以接受多個(gè)參數(shù),并依次輸出他們的值
(println "你所擁有的金幣:" coins))
你所擁有的裝備: [短劍 火把 汽油]
你所擁有的金幣: 256
nil
我們稱之為 解構(gòu)
因?yàn)樗梢郧逦乇憩F(xiàn)原結(jié)構(gòu)的樣子
如果不使用解構(gòu),就會(huì)長(zhǎng)成這樣:
=> (let [items (first my-bag) coins (second my-bag)]
(println "你所擁有的裝備:" items)
(println "你所擁有的金幣:" coins))
你所擁有的裝備: [短劍 火把 汽油]
你所擁有的金幣: 256
nil
解構(gòu)的具體用法十分簡(jiǎn)單
你只需要
在 let 函數(shù)的第二個(gè)參數(shù)里
- 把原來(lái) “給值取的名字” 的位置,寫成一個(gè) vector 的形式(即用中括號(hào)包圍)
- 原來(lái)填寫 “值” 的位置,寫上你要解構(gòu)的集合
(let [[你給集合里元素取的新名字] 你要解構(gòu)的集合])
如果我們把原結(jié)構(gòu)和解構(gòu)形式放在一起觀察的話
my-bag [["短劍" "火把" "汽油"] 256 ]
[ items coins] my-bag
看起來(lái)像是把定義集合倒過(guò)來(lái)寫一樣
這樣我們就在 let 綁定里面給這個(gè)兩個(gè)元素取了一個(gè)名字:
- 集合
my-bag的第一個(gè)元素取名為items - 集合
my-bag的第二個(gè)元素取名為coins
而且,這個(gè)對(duì)應(yīng)關(guān)系真實(shí)反映了元素的位置。
我們可以使用解構(gòu)從集合中取部分元素
=> (def my-bag [["短劍" "火把" "汽油"] 512 2])
#'user/my-bag
=> (let [[items silver-coin] my-bag]
(println "你所擁有的裝備:" items)
(println "你所擁有的銀幣:" silver-coin))
你所擁有的裝備: [短劍 火把 汽油]
你所擁有的銀幣: 512
nil
上面的例子中,雖然我們的 my-bag 有三個(gè)元素,但是解構(gòu)可以只拿取前兩個(gè)
看起來(lái)就像這樣
my-bag [ ["短劍" "火把" "汽油"] 512 2]
[ items silver-coin ] my-bag
如果你只想要物品和金幣,你可以這樣來(lái)操作
=> (let [[items silver-coin gold-coin] my-bag]
(println "你所擁有的裝備:" items)
(println "你所擁有的金幣:" gold-coin))
你所擁有的裝備: [短劍 火把 汽油]
你所擁有的金幣: 2
nil
是不是看起來(lái)有點(diǎn)傻,給它取了名字卻沒有使用。
不過(guò),如果我們不關(guān)心銀幣,那么我們可以給它隨便扔一個(gè)名字
比如 sth-I-don't-care
不過(guò) Clojure 規(guī)范更傾向于使用短下劃線 _ 來(lái)命名你不感興趣的名稱
=> (let [[items _ gold-coin] my-bag]
(println "你所擁有的裝備:" items)
(println "你所擁有的金幣:" gold-coin))
你所擁有的裝備: [短劍 火把 汽油]
你所擁有的金幣: 2
nil
其實(shí)它只也是一個(gè)可以正常使用的名字而已
=> (let [[items _ gold-coin] my-bag]
(println "你所擁有的裝備:" items)
(println "你所擁有的銀幣:" _)
(println "你所擁有的金幣:" gold-coin))
你所擁有的裝備: [短劍 火把 汽油]
你所擁有的銀幣: 512
你所擁有的金幣: 2
nil
如果你使用重復(fù)的名字,比如使用多個(gè) _ 來(lái)忽視掉你不關(guān)心的內(nèi)容
那么后面的值會(huì)把前面的值覆蓋掉
=> (let [[items _ _] my-bag]
(println "你所擁有的裝備:" items)
(println "你所擁有的銀幣:" _)
(println "你所擁有的金幣:" _))
你所擁有的裝備: [短劍 火把 汽油]
你所擁有的銀幣: 2 ;這里金幣的值覆蓋了銀幣的值
你所擁有的金幣: 2
nil
在解構(gòu)多層嵌套時(shí),更能發(fā)揮出它的威力
=> (def my-bag [["短劍" "火把" "汽油"] [512 2]])
=> (let [[[first-item _ third-item] [silver-coin gold-coin]] my-bag]
(println "你背包里的第一件裝備:" first-item)
(println "你背包里的第三件裝備:" third-item)
(println "你所擁有的金幣:" gold-coin)
(println "你所擁有的銀幣:" silver-coin))
你背包里的第一件裝備: 短劍
你背包里的第三件裝備: 汽油
你所擁有的金幣: 2
你所擁有的銀幣: 512
nil
我們像之前一樣對(duì)比一下原集合和解構(gòu)形式
my-bag [[ "短劍" "火把" "汽油" ] [ 512 2 ] ]
[[first-item _ third-item] [silver-coin gold-coin] ] my-bag
這也是我們?yōu)槭裁凑f(shuō),它可以清晰地表現(xiàn)原結(jié)構(gòu)的樣子
解構(gòu)的額外特性
- 給剩余元素取名
=> (def my-bag [["短劍" "火把" "汽油"] 512 2])
=> (let [[[first-item _ third-item] & coins] my-bag]
(println "你背包里的第一件裝備:" first-item)
(println "你背包里的第三件裝備:" third-item)
(println "你所擁有的錢幣:" coins))
你背包里的第一件裝備: 短劍
你背包里的第三件裝備: 汽油
你所擁有的錢幣: (512 2)
nil
使用 & 來(lái)把剩余元素作為一個(gè) list 綁定到一個(gè)名字
在做遞歸調(diào)用的時(shí)候這個(gè)功能用起來(lái)就太爽了
-
給原集合取名
使用
:as來(lái)給你的原集合取名
:as是一個(gè) key (key 會(huì)在今后的介紹中出現(xiàn))
=> (let [[[first-item _ third-item] & coins :as my-bag-original] my-bag]
(println "你背包里的第一件裝備:" first-item)
(println "你背包里的第三件裝備:" third-item)
(println "你所擁有的錢幣:" coins)
(println "全部物品:" my-bag-original))
你背包里的第一件裝備: 短劍
你背包里的第三件裝備: 汽油
你所擁有的錢幣: (512 2)
全部物品: [[短劍 火把 汽油] 512 2]
nil
本文所介紹的解構(gòu)稱為順序解構(gòu),它可以用來(lái)對(duì)順序集合做解構(gòu),包括:
- list,vector
- 實(shí)現(xiàn)了 java.unit.List 接口的集合
- Java 數(shù)組
- 字符串
對(duì)字符串的解構(gòu)結(jié)果是一個(gè)一個(gè)字符
=> (let [[f s t] "123"]
(print s))
2
nil
還有為 map 服務(wù)的解構(gòu)形式,在今后對(duì) map 做單獨(dú)介紹時(shí)再詳細(xì)說(shuō)明
-
可讀性,指人類閱讀程序語(yǔ)言時(shí)的“舒適程度”,“易于理解程度”。讀起來(lái)更容易被理解的程序的可讀性就越高 ?