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"}

正如您所见,disj 不存在的元素是可以的。

检查包含

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 中打印映射时,它将在每个键/值对之间放置 `,’s。这些仅用于可读性 - 逗号在 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

您不应该直接调用映射,除非您能保证它是非空的。

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 函数在映射中找到键/值条目,而不仅仅是值。

键或值

您也可以仅获取映射中的键或仅获取值。

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

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

虽然映射是无序的,但有一个保证,即 keys、vals 和其他按“序列”顺序遍历的函数将始终按相同的顺序遍历特定映射实例的条目。

构建映射

zipmap 函数可用于将两个序列(键和值)“压缩”成一个映射。

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

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

使用 Clojure 的序列函数(我们尚未讨论)还有多种其他方法可以构建映射。稍后再回来看看它们!

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

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

合并映射

merge 函数可用于将多个映射合并成一个映射。

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

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

我们在这里合并了两个映射,但您也可以传递更多映射。

如果两个映射都包含相同的键,则最右边的映射获胜。或者,您可以使用 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}

在发生冲突的情况下,该函数将对两个值进行调用以获取新值。

排序映射

类似于排序集合,排序映射根据比较器按排序顺序维护键,使用 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)

表示应用程序域信息

当我们需要用预先知道的相同字段集来表示许多域信息时,您可以使用具有关键字键的映射。

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

字段访问器

由于这是一个映射,因此我们之前讨论过的通过键查找值的方法也适用。

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

user=> (person :occupation)
"Programmer"

但实际上,获取此使用情况的字段值的的最常用方法是调用关键字。就像映射和集合一样,关键字也是函数。当调用关键字时,它会在传递给它的关联数据结构中查找自身。

user=> (:occupation person)
"Programmer"

关键字调用也采用可选的默认值。

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

更新字段

由于这是一个映射,我们可以使用 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"