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に続く予定。