(def players #{"Alice", "Bob", "Kelly"})
前のセクションで説明したように、Clojureのコレクション型には重要なものが4種類ある: ベクター、リスト、セット、マップだ。4種類のコレクション型のうち、セットとマップはハッシュ化された(hashed)コレクションであり、要素を効率的にルックアップするために設計されている。
セットは数学的な集合(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"}
マップは一般に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-in
や update-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"