Clojure

Clojureを学ぶ - ハッシュ化コレクション

前のセクションで説明したように、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しても問題ありません。

包含の確認

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

ソートされたセット

ソートされたセットは、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では空白として扱われます。必要に応じて自由に使用してください!

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