Clojureのマップ(連想配列)のキホン その1

2011年5月現在、最新Release版である clojure 1.2 を前提にしています。

■マップの種類

Clojureのマップは三種類あります。repl等での表示は同じに見えますが型はそれぞれ違います。

(array-map :a 10 :b 20 :c 30)   ;=> {:a 10, :b 20, :c 30}
(hash-map :a 10 :b 20 :c 30)    ;=> {:a 10, :b 20, :c 30}
(sorted-map :a 10 :b 20 :c 30)  ;=> {:a 10, :b 20, :c 30}

(type (array-map :a 10 :b 20 :c 30))  ;=> clojure.lang.PersistentArrayMap
(type (hash-map :a 10 :b 20 :c 30))   ;=> clojure.lang.PersistentHashMap
(type (sorted-map :a 10 :b 20 :c 30)) ;=> clojure.lang.PersistentTreeMap

リテラルで書いたマップは ArrayMap になります。

(type {:a 10 :b 20 :c 30}) ;=> clojure.lang.PersistentArrayMap

3つのマップ型は要素の並び順が異ります。
array-map は入力順(後述)、sorted-map はキー昇順に並びます。hash-map は人間が見て理解できる並び順にはなりません。
参考:マップを作りたい@逆引きClojure

■各種判定

マップ型は、コレクションですが、シーケンス型ではありません。

(map? {:a 10 :b 20 :c 30})  ;=> true
(coll? {:a 10 :b 20 :c 30}) ;=> true

(vector? {:a 10 :b 20 :c 30})     ;=> false
(list? {:a 10 :b 20 :c 30})       ;=> false
(seq? {:a 10 :b 20 :c 30})        ;= false
(sequential? {:a 10 :b 20 :c 30}) ;=> false

(sorted? (array-map :a 10 :b 20))  ;=> false
(sorted? (hash-map :a 10 :b 20))   ;=> false
(sorted? (sorted-map :a 10 :b 20)) ;=> true

■マップのキーの型

immutable な型はすべてマップのキーにできます。

;; 数値
{6 "バレーボール", 9 "野球", 11 "サッカー"}

;; 文字列
{"03" "東京", "052" "名古屋", "06" "大阪"} 

;; ベクター
{["295" "0102"] "千葉県南房総市白浜町白浜"
 ["064" "0941"] "和歌山県西牟婁郡白浜町安居"
 ["672" "8023"] "兵庫県姫路市白浜町"}

実はキーの型はすべて一致している必要はありません。ただしデフォルトの比較が定義されていない型同士は sorted-map でエラーになります。

{:a 10, 'a 20, "a" 30, ['a] 40}            ;=> {:a 10, a 20, "a" 30, [a] 40}
(hash-map :a 10, 'a 20, "a" 30, ['a] 40)   ;=> {"a" 30, :a 10, [a] 40, a 20}
(sorted-map :a 10, 'a 20, "a" 30, ['a] 40) ;=> ERROR (これは NG)

■sorted-mapに比較ルールを与える (sorted-map-by)

sorted-map で意図した順序にならない場合、sorted-map-by を使います。

;; 文字列だと辞書順になってしまう。
(sorted-map "8" 'x "9" 'y "10" 'z)
;;=> {"10" z, "8" x, "9" y}
;; parseInt してから比較するコンパレータを sorted-map-by に与えれば、
;; 数値として比較できる。
(let [f #(compare (Integer/parseInt %1) (Integer/parseInt %2))]
  (sorted-map-by f "8" 'x "9" 'y "10" 'z))
;;=> {"8" x, "9" y, "10" z}

キーが本来比較不能な型であっても sorted-map-by でコンパレータを与えれば sorted-map が作れます。

(let [f #(compare (str [(type %1) %1]) (str [(type %2) %2]))]
  (sorted-map-by f :a 10 "a" 20 'a 30 ['a] 40))
;;=> {:a 10, [a] 40, a 30, "a" 20}

■キーによる検索その1 (get等)

clojure では、マップ型および、キーワード型は、callableです。式の最初に書くことで関数として振舞います。評価値は「指定したキーに対応する値」です。

;; マップを関数のように扱う
({:a 10 :b 20 :c 30} :b)  ;=> 20
({:a 10 :b 20 :c 30} :z)  ;=> nil

;; キーワードを関数のように扱う
(:b {:a 10 :b 20 :c 30})  ;=> 20
(:z {:a 10 :b 20 :c 30})  ;=> nil

式の3つめの要素は、検索に失敗した時に返すデフォルト値になります。nil 以外を返したい時に使います。

({:a 10 :b 20 :c 30} :z -1)  ;=> -1
(:z {:a 10 :b 20 :c 30} -1)  ;=> -1

なおキーワード型でないキーは、関数のようには扱えません。

("b" {"a" 10 "b" 20 "c" 30})   ;=> ERROR (これは NG)

ただし、マップの方はキーの型によらず関数のように扱えます。

({"a" 10 "b" 20 "c" 30} "b")   ;=> 20

関数 get でも、検索が出来ます。

(get {"a" 10 "b" 20 "c" 30} "b")     ;=> 20
(get {"a" 10 "b" 20 "c" 30} "z")     ;=> nil
(get {"a" 10 "b" 20 "c" 30} "z" -1)  ;=> -1

■キーによる検索その2 (find, select-keys)

find は検索でヒットした要素を、キーと値のペアで返します。
find では、第3引数に検索失敗時のデフォルト値が指定できません。

(find {:a 10 :b 20 :c 30} :b)     ;=> [:b 2]
(find {:a 10 :b 20 :c 30} :z)     ;=> nil 
(find {:a 10 :b 20 :c 30} :z -1)  ;=> ERROR (これは NG)

select-keys は 該当する要素をマップの形で返します。複数のキーが指定できます。

(select-keys {:a 10 :b 20 :c 30} [:a :c])    ;=> {:c 30, :a 10}
(select-keys {:a 10 :b 20 :c 30} [:a :z])    ;=> {:a 10}
(select-keys {:a 10 :b 20 :c 30} [:x :y])    ;=> {}
(select-keys {:a 10 :b 20 :c 30} [:x :y] -1) ;=> ERROR (これは NG)

■キー列挙、値列挙

キー列挙は keys、値列挙は vals です。

(keys {:a 10 :b 20 :c 30})  ;=> (:a :b :c)
(vals {:a 10 :b 20 :c 30})  ;=> (10 20 30)

keys, vals は、3つのマップ型の並び順の違いを結果に反映します。

(keys (array-map  :a 10 :c 30 :b 20 :z 99)) ;=> (:a :c :b :z)
(keys (hash-map   :a 10 :c 30 :b 20 :z 99)) ;=> (:z :a :c :b)
(keys (sorted-map :a 10 :c 30 :b 20 :z 99)) ;=> (:a :b :c :z)

(vals (array-map  :a 10 :c 30 :b 20 :z 99)) ;=> (10 30 20 99)
(vals (hash-map   :a 10 :c 30 :b 20 :z 99)) ;=> (99 10 30 20)
(vals (sorted-map :a 10 :c 30 :b 20 :z 99)) ;=> (10 20 30 99)

■マップ型をシーケンスとして扱う

マップはシーケンスではありませんが、シーケンスを期待する文脈では「キーと値のペア」を要素とするシーケンスに暗黙に変換されます。

(apply list {:a 10 :b 20 :c 30}) ;=> ([:a 10] [:b 20] [:c 30])
(first {:a 10 :b 20 :c 30})    ;=> [:a 10]
(ffirst {:a 10 :b 20 :c 30})   ;=> :a

「キーと値のペア」は一見 vector のように見えますが、type は MapEntry型です。
ただし、MapEntry は APersistentVector を継承しているので、結局のところ vector と同じとみなせます。

(def x (first {:a 10 :b 20 :c 30})) ;=> [:a 10]
(type x)    ;=> clojure.lang.MapEntry

(vector? x) ;=> true
(x 1)       ;=> 10

javaのソースをみると、MapEntry 自身は setter/getter だけを実装するシンプルな抽象クラスですが、スーパークラスをさかのぼってみると、APersistentVector, AFn, Comparable, Streamable など色々継承していて興味深いです。
MapEntry.java (Google Code Search)

シーケンスに変換されるので、シーケンスを対象とする関数でも当然扱えます。

(take-while (fn [[k v]] (< v 25))
            {:a 10 :b 20 :c 30})     ;=> ([:a 10] [:b 20])

(reduce (fn [acc [k v]] (+ acc v))
        0
        {:a 10 :b 20 :c 30})         ;=> 60

(apply array-map
       (mapcat #(list %1 (second %2))
               [:x :y]
               {:a 10 :b 20 :c 30})) ;=> {:x 10, :y 20}

■マップ要素の追加と更新 (assoc)

追加/更新と言っても、Clojure のマップはイミュータブル(変更不可)なので破壊的操作はできません。値の異る新なインスタンスを生成してそれを返します。元データは変更されません。

(assoc {:a 10 :b 20 :c 30} :b 22)       ;=> {:a 10, :b 22, :c 30}
(assoc {:a 10 :b 20 :c 30} :z 99)       ;=> {:z 99, :a 10, :b 20, :c 30}
(assoc {:a 10 :b 20 :c 30} :b 22 :z 99) ;=> {:z 99, :a 10, :b 22, :c 30}

上の最後の式のように assoc は可変長引数で、複数の要素更新をまとめて行えます。

ところで追加された要素が、マップの末尾ではなく先頭に追加されているのが気になりますね。
既にのべたように、array-map の要素並びは「追加順」です。
そして次のコードを見ればわかりますが、array-map では追加要素はマップの先頭に入っていくようです。これが「追加順」です。

(assoc (assoc (assoc {} :a 10) :b 20) :c 30)
;;=> {:c 30, :b 20, :a 10}

(reduce (fn [m [k v]] (assoc m k v)), {}, [[:a 10] [:b 20] [:c 30]])
;;=> {:c 30, :b 20, :a 10}

(assoc {} :a 10 :b 20 :c 30)
;;=> {:c 30, :b 20, :a 10}

特に直感に反する事はありませんね。「先頭に追加」されるという点だけ注意です。

sorted-map の場合、当然ですがキー昇順が保たれます。どういう順序で追加しても同じです。

(assoc (sorted-map) :a 10 :b 20 :c 30)
;;=> {:a 10, :b 20, :c 30}

(assoc (sorted-map) :b 20 :a 10 :c 30)
;;=> {:a 10, :b 20, :c 30}

(assoc (sorted-map) :c 30 :b 20 :a 10)
;;=> {:a 10, :b 20, :c 30}

■マップ要素の削除 (dissoc)

これは説明不要。

(dissoc {:a 10 :b 20 :c 30} :b)       ;=> {:a 10, :c 30}
(dissoc {:a 10 :b 20 :c 30} :b :c)    ;=> {:a 10}
(dissoc {:a 10 :b 20 :c 30} :b :c :z) ;=> {:a 10}

assoc 同様、元データは破壊しません。

■ソート(sort)

sort関数の引数はシーケンスなので、マップを引数にすると MapEntry のシーケンスに変換してのソートになります。一応ソートできますが、多くの場合本来の意図とは違う結果になるでしょう。

(sort {:c 30 :a 10 :b 20})
;;=> ([:a 10] [:b 20] [:c 30])

(sort #(compare %2 %1) {:c 30 :a 10 :b 20})
;;=> ([:c 30] [:b 20] [:a 10])

sorted-map や、hash-map では意図的にソート処理を行う意味がありません。強いていえば以下のように array-map で並び順をソートするのに使える程度でしょうか。

(let [m {:c 30 :a 10 :b 20}]
  (apply assoc {} (apply concat (sort #(compare %2 %1) m))))
;;=> {:a 10, :b 20, :c 30}

そんなことしなくても、sorted-map をつかった方がシンプルにできます。

(let [m {:c 30 :a 10 :b 20}]
  (apply sorted-map (apply concat m)))
;;=> {:a 10, :b 20, :c 30}

■マップを要素にもつシーケンスのソート(sort-by)

マップをレコードとするデータベースのようなシーケンスをソートするのに sort-by が使えます。
第1引数で、ソートのキーを指定します。

(sort-by :age [{:name "相生祐子" :age 15}
               {:name "東雲なの" :age 1}
               {:name "はかせ" :age 8}])
;;=> ({:name "東雲なの", :age 1} {:name "はかせ", :age 8} {:name "相生祐子", :age 15})

第2引数で比較関数をあたえることもできます。

(sort-by :age > [{:name "相生祐子" :age 15}
                 {:name "東雲なの" :age 1}
                 {:name "はかせ" :age 8}])
;;=> ({:name "相生祐子", :age 15} {:name "はかせ", :age 8} {:name "東雲なの", :age 1})

その2に続く予定。