(def players #{"Alice", "Bob", "Kelly"})
前のセクションで説明したように、Clojureには4つの主要なコレクション型があります:ベクター、リスト、セット、およびマップ。 それらの4つのコレクション型のうち、セットとマップはハッシュ化コレクションであり、要素の効率的なルックアップのために設計されています。
セットは数学的なセットのように、順序がなく、重複がありません。セットは、コレクションに要素が含まれているかどうかを効率的に確認したり、任意の要素を削除したりするのに理想的です。
(def players #{"Alice", "Bob", "Kelly"})
ベクターやリストと同様に、要素を追加するためにconj
が使用されます。
user=> (conj players "Fred")
#{"Alice" "Fred" "Bob" "Kelly"}
disj
(「分離」)関数は、セットから1つ以上の要素を削除するために使用されます。
user=> players
#{"Alice" "Kelly" "Bob"}
user=> (disj players "Bob" "Sal")
#{"Alice" "Kelly"}
ご覧のとおり、セットに存在しない要素をdisj
しても問題ありません。
マップは一般的に2つの目的で使用されます。キーと値の関連付けを管理することと、ドメインアプリケーションデータを表現することです。最初のユースケースは、他の言語ではしばしば辞書またはハッシュマップと呼ばれます。
マップは、{
と}
で囲まれた交互のキーと値として表されます。
(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
値を持つ既存のキーを区別するのにも役立ちます。
マップにエントリが含まれているかどうかを確認するのに役立つ他の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)
マップは順序付けられていませんが、キー、値、および「シーケンス」順序でウォークする他の関数は、常に特定のマップインスタンスのエントリを同じ順序でウォークすることが保証されています。
zipmap
関数は、2つのシーケンス(キーと値)をマップに「zip」するために使用できます
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}
ここでは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}
競合の場合、新しい値を取得するために両方の値に対して関数が呼び出されます。
ソートされたセットと同様に、ソートされたマップは、デフォルトの比較関数として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-in
またはupdate-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"