Clojure

學習 Clojure - 哈希集合

如前一節所述,有四種主要的 Clojure 集合類型:向量、清單、集合和映射。在這四種集合類型中,集合和映射是哈希集合,旨在有效查詢元素。

集合

集合就像數學集合 - 無序且沒有重複。集合非常適合有效檢查集合是否包含元素,或移除任何任意元素。

(def players #{"Alice", "Bob", "Kelly"})

新增到集合

與向量和清單一樣,conj 用於新增元素。

user=> (conj players "Fred")
#{"Alice" "Fred" "Bob" "Kelly"}

從集合中移除

disj(「分離」)函數用於從集合中移除一個或多個元素。

user=> players
#{"Alice" "Kelly" "Bob"}
user=> (disj players "Bob" "Sal")
#{"Alice" "Kelly"}

如你所見,從集合中移除不存在的元素並無妨。

檢查包含

user=> (contains? players "Kelly")
true

已排序的集合

已排序的集合會根據比較器函數進行排序,該函數可以比較兩個元素。預設情況下,Clojure 的 compare 函數會用於對數字、字串等進行「自然」排序。

user=> (conj (sorted-set) "Bravo" "Charlie" "Sigma" "Alpha")
#{"Alpha" "Bravo" "Charlie" "Sigma"}

也可以使用 sorted-set-by 搭配自訂比較器。

into

into 用於將一個集合放入另一個集合中。

user=> (def players #{"Alice" "Bob" "Kelly"})
user=> (def new-players ["Tim" "Sue" "Greg"])
user=> (into players new-players)
#{"Alice" "Greg" "Sue" "Bob" "Tim" "Kelly"}

into 會傳回與其第一個引數相同類型的集合。

映射

映射通常用於兩個目的 - 管理鍵值關聯以及表示網域應用程式資料。第一個使用案例在其他語言中通常稱為字典或雜湊映射。

建立文字映射

映射表示為交替的鍵和值,並以 {} 包圍。

(def scores {"Fred"  1400
             "Bob"   1240
             "Angela" 1024})

當 Clojure 在 REPL 中列印映射時,它會在每個鍵/值對之間加上逗號。這些純粹用於可讀性 - 逗號在 Clojure 中視為空白。在有助於理解的情況下,請隨時使用它們!

;; same as the last one!
(def scores {"Fred" 1400, "Bob" 1240, "Angela" 1024})

新增鍵值對

使用 assoc(「關聯」的簡寫)函數將新值新增到映射中

user=> (assoc scores "Sally" 0)
{"Angela" 1024, "Bob" 1240, "Fred" 1400, "Sally" 0}

如果 assoc 中使用的鍵已存在,則會取代該值。

user=> (assoc scores "Bob" 0)
{"Angela" 1024, "Bob" 0, "Fred" 1400}

移除鍵值對

移除鍵值對的互補操作是 dissoc(「分解」)

user=> (dissoc scores "Bob")
{"Angela" 1024, "Fred" 1400}

根據鍵查詢

有幾種方法可以在映射中查詢值。最明顯的是 get 函數

user=> (get scores "Angela")
1024

當問題中的映射被視為常數查詢表時,通常會呼叫映射本身,並將其視為函數

user=> (def directions {:north 0
                        :east 1
                        :south 2
                        :west 3})
#'user/directions

user=> (directions :north)
0

除非你可以保證映射不會為 nil,否則不應直接呼叫映射

user=> (def bad-lookup-map nil)
#'user/bad-lookup-map

user=> (bad-lookup-map :foo)
Execution error (NullPointerException) at user/eval154 (REPL:1).
null

使用預設值查詢

如果你想執行查詢,並在找不到鍵時回退至預設值,請將預設值指定為額外的參數

user=> (get scores "Sam" 0)
0
​
user=> (directions :northwest -1)
-1

使用預設值也有助於區分遺失的鍵和具有 nil 值的現有鍵。

檢查包含

還有兩個函數有助於檢查映射是否包含某個項目。

user=> (contains? scores "Fred")
true

user=> (find scores "Fred")
["Fred" 1400]

contains? 函數是檢查包含的謂詞。find 函數會在映射中找到鍵/值項目,而不仅仅是值。

鍵或值

你也可以只取得 map 中的鍵或值

user=> (keys scores)
("Fred" "Bob" "Angela")

user=> (vals scores)
(1400 1240 1024)

雖然 map 是無序的,但可以保證鍵、值和其他以「順序」順序運行的函式,將永遠以相同的順序遍歷特定 map 實例中的項目。

建立 map

zipmap 函式可用於將兩個順序(鍵和值)「壓縮」成一個 map

user=> (def players #{"Alice" "Bob" "Kelly"})
#'user/players

user=> (zipmap players (repeat 0))
{"Kelly" 0, "Bob" 0, "Alice" 0}

還有許多其他方法可以使用 Clojure 的順序函式建立 map(我們尚未討論)。稍後再回來討論這些方法!

;; with map and into
(into {} (map (fn [player] [player 0]) players))

;; with reduce
(reduce (fn [m player]
          (assoc m player 0))
        {} ; initial value
        players)

合併 map

merge 函式可用於將多個 map 合併成一個單一 map

user=> (def new-scores {"Angela" 300 "Jeff" 900})
#'user/new-scores

user=> (merge scores new-scores)
{"Fred" 1400, "Bob" 1240, "Jeff" 900, "Angela" 300}

我們在此合併了兩個 map,但你也可以傳遞更多 map。

如果兩個 map 包含相同的鍵,最右邊的 map 會獲勝。或者,你可以使用 merge-with 提供一個函式,在發生衝突時呼叫該函式

user=> (def new-scores {"Fred" 550 "Angela" 900 "Sam" 1000})
#'user/new-scores

user=> (merge-with + scores new-scores)
{"Sam" 1000, "Fred" 1950, "Bob" 1240, "Angela" 1924}

在發生衝突時,函式會針對兩個值呼叫,以取得新的值。

已排序的 map

類似於已排序的集合,已排序的 map 會根據比較器以已排序的順序維護鍵,使用 compare 作為預設的比較器函式。

user=> (def sm (sorted-map
         "Bravo" 204
         "Alfa" 35
         "Sigma" 99
         "Charlie" 100))
{"Alfa" 35, "Bravo" 204, "Charlie" 100, "Sigma" 99}

user=> (keys sm)
("Alfa" "Bravo" "Charlie" "Sigma")

user=> (vals sm)
(35 204 100 99)

表示應用程式網域資訊

當我們需要使用一組已知且相同的欄位表示許多網域資訊時,你可以使用具有關鍵字鍵的 map。

(def person
  {:first-name "Kelly"
   :last-name "Keen"
   :age 32
   :occupation "Programmer"})

欄位存取器

由於這是一個 map,因此我們之前討論過用鍵查詢值的那些方法也適用

user=> (get person :occupation)
"Programmer"

user=> (person :occupation)
"Programmer"

但實際上,取得此用途的欄位值的常見方法是呼叫關鍵字。就像 map 和集合一樣,關鍵字也是函式。當呼叫關鍵字時,它會在傳遞給它的關聯式資料結構中查詢自己。

user=> (:occupation person)
"Programmer"

關鍵字呼叫也需要一個選用的預設值

user=> (:favorite-color person "beige")
"beige"

更新欄位

由於這是一個 map,我們可以使用 assoc 來新增或修改欄位

user=> (assoc person :occupation "Baker")
{:age 32, :last-name "Keen", :first-name "Kelly", :occupation "Baker"}

移除欄位

使用 dissoc 來移除欄位

user=> (dissoc person :age)
{:last-name "Keen", :first-name "Kelly", :occupation "Programmer"}

巢狀實體

常見到實體巢狀在其他實體中

(def company
  {:name "WidgetCo"
   :address {:street "123 Main St"
             :city "Springfield"
             :state "IL"}})

你可以使用 get-in 來存取巢狀實體中任何層級的欄位

user=> (get-in company [:address :city])
"Springfield"

您也可以使用 assoc-inupdate-in 來修改巢狀實體

user=> (assoc-in company [:address :street] "303 Broadway")
{:name "WidgetCo",
 :address
 {:state "IL",
  :city "Springfield",
  :street "303 Broadway"}}

記錄

使用記錄是使用映射的替代方案。記錄是專門為此用例而設計的,通常具有更好的效能。此外,它們有一個名為「類型」的名稱,可用於多型行為(稍後會詳細說明)。

記錄使用記錄實例的欄位名稱清單來定義。這些將在每個記錄實例中視為關鍵字鍵。

;; Define a record structure
(defrecord Person [first-name last-name age occupation])

;; Positional constructor - generated
(def kelly (->Person "Kelly" "Keen" 32 "Programmer"))

;; Map constructor - generated
(def kelly (map->Person
             {:first-name "Kelly"
              :last-name "Keen"
              :age 32
              :occupation "Programmer"}))

記錄的使用方式幾乎與映射相同,但它們無法像映射一樣被呼叫為函式。

user=> (:occupation kelly)
"Programmer"