セット

セットは数学的な集合(set)のようなものだ――順序付けられておらず重複がない。セットはコレクションが要素を含んでいるかどうかを効率的に確かめたり任意の要素を取り除いたりするのに最適だ。

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

セットに追加する

ベクターやリストと同様に、 conj が要素を追加するために使われる。

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

セットから取り除く

disj ("disjoin")関数はセットから1個以上の要素を取り除くために使われる。

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

含まれていることを確認する

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

ソート済みセット

ソート済み(sorted)セットは2つの要素を比較可能なコンパレータ関数に従ってソートされている。デフォルトでは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 は最初の引数と同じ型のコレクションを返す。

マップ

マップは一般に2通りの目的で使われる――キーから値への関連付けを管理するためとドメインのアプリケーションデータを表現するためだ。最初のユースケースは他の言語ではディクショナリやハッシュマップと呼ばれることが多い。

リテラルのマップを作る

マップは交互に並んだキーと値が {} に囲まれて表現される。

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

ClojureがREPLでマップを出力する際には、個々のキー/値のペアの間に , を入れる。これは純粋に可読性のために使われている――Clojureではカンマは空白として扱われる。役に立つ場合には自由に使おう!

;; 先ほどのものと同じ!
(def scores {"Fred" 1400, "Bob" 1240, "Angela" 1024})

新たなキー-値のペアを追加する

新たな値は assoc ("associate"の略)関数でマップに追加される:

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("dissociate")だ:

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 値であることとを区別するのにも役に立つ。

含まれていることを確認する

マップにエントリが含まれているかどうかを確認するのに便利な関数が他に2つある。

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 関数は2つのシーケンス(キーと値)をジッパーを閉じるようにまとめ("zip")てマップにするのに使うことができる:

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

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

Clojureのシーケンス関数を使ってマップを構築するには他にも様々な方法がある(まだ述べていないが)。これにはあとで戻ってこよう!

;; map と into で
(into {} (map (fn [player] [player 0]) players))

;; reduce で
(reduce (fn [m player]
          (assoc m player 0))
        {} ; 初期値
        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}

ここでは2つのマップをマージしたが、より多くのマップを渡すこともできる。

両方のマップが同じキーを含んでいると、最も右のマップが勝つ。あるいは衝突があった場合に呼び出される関数を与える 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}

衝突があった場合には、両者の値に対してその関数が呼び出されて新たな値が得られる。

ソート済みマップ

ソート済みセットと同様に、ソート済み(sorted)マップではコンパレータに基づいてキーがソート済みの順序で維持され、デフォルトのコンパレータ関数としては 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"

しかし実は、この用途でフィールドの値を得る最も一般的な方法はキーワードを呼び出すことによるものだ。マップやセットと同じようにキーワードもまた関数だ。キーワードが呼び出されると、渡された連想的(associative)なデータ構造からキーワード自身をルックアップする。

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

レコード

マップを使うことの代替手段は「レコード」を作ることだ。レコードは特にこのユースケースのために設計されており、一般にパフォーマンスが優れている。加えて、レコードには名前付きの「型」があり、ポリモーフィックな振る舞いのために使うことができる(詳しくは後ほど)。

レコードは、レコードインスタンスのフィールド名のリストとともに定義される。このフィールド名は個々のレコードインスタンスでキーワードのキーとして扱われる。

;; レコード構造を定義する
(defrecord Person [first-name last-name age occupation])

;; 位置引数によるコンストラクタ――自動生成される
(def kelly (->Person "Kelly" "Keen" 32 "Programmer"))

;; マップによるコンストラクタ――自動生成される
(def kelly (map->Person
             {:first-name "Kelly"
              :last-name "Keen"
              :age 32
              :occupation "Programmer"}))

レコードはマップとほとんど全く同じように使えるが、マップのように関数として呼び出すことはできないことに注意が必要だ。

user=> (:occupation kelly)
"Programmer"