Clojure 零基礎 學習筆記 map
map 是一種映射關系
map 長啥樣
這次介紹的 map 不是高階函數(shù) map ,而是我們在第二篇文章《你好,集合》里面曾經(jīng)提到名字的一種 Clojure 提供的數(shù)據(jù)結構。高階函數(shù) map 像是一個動詞,表示一種函數(shù)和一系列參數(shù)之間的“映射”動作。而數(shù)據(jù)結構 map 是一個名詞,它表示“關鍵字”和“值”之間的對應關系。也就是業(yè)界通常簡稱的“鍵值對”、key-value。
那到底長啥樣呢?我們來寫幾個 map
{"鍵" "值"}
{"關鍵字1" "值1" "關鍵字2" "值2"} ;不推薦這種使用空格來間隔的寫法
{:name "blindingdark", :age "不告訴你"} ;如果寫在一行,每一組鍵值對之間用逗號間隔
{:name "FFF團火法師"
:level 32
:coins 128
:items ["生銹的普通匕首" "火把" "汽油"]} ;不過我們更建議在每組鍵值對之間換行
接觸過 JSON 的同學應該非常熟悉這種形式。
使用花括號 {} 來包圍一組鍵值對,鍵和值可以是任意表達式(如字符串,數(shù)字,list,vector)。每一對鍵值對中,鍵和值用空格隔開,不同的鍵值對之間使用逗號或者空格或者換行隔開。
上面的例子中也對間隔符號進行了說明,雖然 Clojure 允許多種個性化的風格,但無論哪種風格,總是要保持良好的可讀性。
不過通常我們使用一種特別的形式來表示“鍵”,即使用 冒號+關鍵字名 的形式來表示。你馬上就會在接下來的內(nèi)容中看到,如何使用這種形式更方便的訪問 map 中的元素。
map 中的 key 必須保證唯一性!
如果你在一個 map 里使用了重名的 key,那么就會報錯。
=> {"鍵" "值1"
"鍵" "值2"}
IllegalArgumentException Duplicate key: 鍵 clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:70)
操作 map 元素
首先我們假設有一名玩家的信息是這樣的
(def player-info {:name "FFF團火法師"
:level 32
:coins 128
:items ["生銹的普通匕首" "火把" "汽油"]})
那么我們怎么去得到這個用戶的信息呢?
我們可以使用 get 函數(shù)來得到某個 key 的 value。
小貼士:使用英文可以有效提高逼格
=> (get player-info :items)
["生銹的普通匕首" "火把" "汽油"]
=> (get player-info :level)
32
=> (get player-info :sex)
nil
=> (get player-info :sex "沒有找到這個屬性")
"沒有找到這個屬性"
很容易就可以總結出 get 函數(shù)的用法:
- 第一個參數(shù)是要訪問的 map
- 第二個參數(shù)是要訪問的 map 中的 key
- 第三個參數(shù)可選,作用是設置一個默認值,如果 map 中訪問的 key 不存在,則返回這個參數(shù)的值
- 正常情況下返回 key 所對應的 value
- 如果第三個參數(shù)未填寫,那么一旦 map 中沒有所訪問的 key,則返回
nil
get 函數(shù)不僅能訪問 map,有關它的其他用法請查詢相關 api。
如果你使用了 冒號+關鍵字名 這種形式來表示一個 key,那么我們就可以更加簡便的訪問 map 中的 value 了。
=> (:name player-info)
"FFF團火法師"
=> (:sex player-info)
nil
=> (:sex player-info "not found")
"not found"
我們發(fā)現(xiàn), 冒號+關鍵字名 這種形式的表示,竟然能放在括號的第一個位置,這說明它也是一個函數(shù)。所以這種奇特的函數(shù)的使用方法是這樣的:
- 第一個參數(shù)是要訪問的 map
- 第二個參數(shù)可選(和
get函數(shù)的第三個參數(shù)作用一致) - 在 map 中尋找和 key 函數(shù)本身相同的 key(其它特性與
get函數(shù)相似)
除了讀取 map 中的內(nèi)容,我們還可以使用 assoc 和 dissoc 來增加和刪除 map 中的元素。
比如我們的火法師成功的燒燒燒了一對情侶,獲得 100 金幣,獲得稱號:大師級火焰掌控
首先,我們使用 assoc 函數(shù)向 player-info 添加一個鍵值對 稱號-稱號名
=> (assoc player-info :achieve "大師級火焰掌控")
{:name "FFF團火法師", :level 32, :coins 128, :items ["生銹的普通匕首" "火把" "汽油"], :achieve "大師級火焰掌控"}
還記得我們之前說過的使用 def 聲明的值是不可變的 么?所以此時我們訪問 player-info 會發(fā)現(xiàn)它并沒有改變。
=> player-info
{:name "FFF團火法師", :level 32, :coins 128, :items ["生銹的普通匕首" "火把" "汽油"]}
所以我們得重新聲明:
=> (def player-info (assoc player-info :achieve "大師級火焰掌控"))
#'user/player-info
如果 assoc 函數(shù)所添加的 key 已經(jīng)存在,就會使用新值來覆蓋。所以我們在此使用它來改變金幣的數(shù)量。
=> (assoc player-info :coins (+ 100 (:coins player-info)))
{:name "FFF團火法師", :level 32, :coins 228, :items ["生銹的普通匕首" "火把" "汽油"], :achieve "大師級火焰掌控"}
當然,它還是沒有改變 player-info 的值。
我們還可以一次性對 map 進行多個值的修改,只需要把需要添加的鍵值對依次寫上:
=> (assoc player-info
:coins (+ 100 (:coins player-info))
:achieve "大師級火焰掌控")
{:name "FFF團火法師", :level 32, :coins 228, :items ["生銹的普通匕首" "火把" "汽油"], :achieve "大師級火焰掌控"}
如果要刪除某個鍵值對,我們可以使用 dissoc 函數(shù)。比如我們刪除稱號:
=> (dissoc player-info :achieve)
{:name "FFF團火法師", :level 32, :coins 228, :items ["生銹的普通匕首" "火把" "汽油"]}
如果我們想得到所有的 key 或者得到所有的 value,可以用 keys 函數(shù)和 vals 函數(shù):
=> (keys player-info)
(:name :level :coins :items)
=> (vals player-info)
("FFF團火法師" 32 128 ["生銹的普通匕首" "火把" "汽油"])
map 嵌套
map 中可以嵌套 map(或者嵌套任何你想要的表達式),這是非常自然的做法,你可以一層一層的來組裝你的數(shù)據(jù),以便更好的描述你所需要的內(nèi)容。
比如我們可以給我們的 player-info 添加一條信息,來表示已裝備的護甲:
=> (assoc player-info :armor {:head "巫師帽子", :body "黑色法袍"})
{:name "FFF團火法師",
:level 32,
:coins 228,
:items ["生銹的普通匕首" "火把" "汽油"],
:achieve "大師級火焰掌控",
:armor {:head "巫師帽子", :body "黑色法袍"}}
訪問它的方式大家就開動腦筋吧。
map 解構
還記得之前講到的順序解構么?map 的解構也差不多:
=> (let [{n :name,coins :coins} player-info]
(println n)
(println coins))
FFF團火法師
228
nil
只不過要注意,鍵值對解構要把 key 寫在后面,而給 key 的 value 取的新名字寫在前面。通常我們把 value 的新名字取的和 key 一樣,當然也可以不一樣。
再來看看 defn 函數(shù)中的解構樣式(點這里可以復習如何解構參數(shù)列表)
讓我們寫一個函數(shù),來顯示我們的火法師的姓名和等級:
=> (defn show-level-and-name
[{name :name,coins :coins}]
(println "昵稱:" name)
(println "金幣:" coins))
#'user/show-level-and-name
=> (show-level-and-name player-info)
昵稱: FFF團火法師
金幣: 228
nil
同樣我們可以使用嵌套的 map 解構:
=> (defn show-armors
[{{head :head,body :body} :armor}]
(println "頭部:" head)
(println "身體:" body))
#'user/show-armors
=> (show-armors player-info)
頭部: 巫師帽子
身體: 黑色法袍
nil
我們甚至可以把順序解構和 map 解構結合起來,顯示我們道具欄中的物品:
=> (let [{name :name,coins :coins,[i1 i2 i3] :items} player-info]
(println name)
(println coins)
(println i1 i2 i3))
FFF團火法師
228
生銹的普通匕首 火把 汽油
nil
上面我們說了,解構的時候我們習慣把解構之后的名字取成和 key 一樣,這樣一來,我們就要把一個名字寫兩遍,當元素增多的時候,重復勞動的負擔就無法忍受了。
如果你的 key 都使用 冒號+名字 的格式,你就可以使用 Clojure 提供的另一種方法:
=> (let [{:keys [coins name]} player-info]
(println "昵稱:" name)
(println "金幣:" coins))
昵稱: FFF團火法師
金幣: 128
nil
對比以前 [{name :name,coins :coins} player-info] ,新形式使用 :keys 放在了 map 解構頭部,然后跟上一個 vector ,vector 里面的名字要和 key 的名字一致,但順序無要求。
還有許多解構的特殊形式,如解構剩余內(nèi)容,解構的默認值,在今后的學習中在進行說明。