M.Hiroi's Home Page

Clojure Programming

お気楽 Clojure プログラミング超入門


Copyright (C) 2025 Makoto Hiroi
All rights reserved.

マップとセット

前回は「ベクタ (vector)」について説明しました。今回は Clojure の基本的なコレクションである「マップ (Map)」と「セット (Set)」について説明します。

●マップとは?

マップは「連想配列」のことで、Perl や Ruby では「ハッシュ (Hash)」と呼ばれています。ベクタが整数値を使って要素を指定するのに対し、マップはキー (key) というデータを使って要素を指定します。一般に、連想配列のキーには文字列が用いられますが、Clojure では immutable なデータであれば、マップのキーとして使用することができます。

Perl や Ruby で連想配列をハッシュと呼ぶのは、連想配列を実現するアルゴリズムに「ハッシュ法 (hashing)」が用いられているからです。Clojure の場合、マップは不変 (immutable) なデータ型で、Array Map, Hash Map, Sorted Map の 3 種類があります。Array Map は配列を使った単純な実装方法で、Hash Map はハッシュ法を、Sorted Map は木構造を使って実装されています。

ハッシュ法はデータを高速に探索できるアルゴリズムです。線形探索では時間がとてもかかる処理でも、マップを遣うと高速にデータを探索することができます。ハッシュ法 (ハッシュ表) のアルゴリズムに興味のある方は、拙作のページ Algorithms with Python 「ハッシュ法」をお読みくださいませ。

●マップの生成

マップは次の方法で作成します。

マップは波カッコ '{' と '}' で囲み、キー key と値 value を空白で区切って表します。{ } は要素が一つもない空マップになります。{ ... } は read で読み込むことができる形式で、プログラムの中で指定することもできます。生成されるマップは Array Map です。関数 array-map は Array Map を、hash-map は Hash Map を、sorted-map は Sorted Map を生成します。sorted-map-by は引数 comp に比較関数を渡します。

簡単な例を示しましょう。

user=> (def a {:foo 10 :bar 20 :baz 30})
#'user/a
user=> a
{:foo 10, :bar 20, :baz 30}

user=> (type a)
clojure.lang.PersistentArrayMap

user=> (array-map :bar 20, :foo 10, :baz 30)
{:bar 20, :foo 10, :baz 30}
user=> (first a)
[:foo 10]
user=> (first (array-map :bar 20, :foo 10, :baz 30))
[:bar 20]
user=> (= a (array-map :bar 20, :foo 10, :baz 30))
true

user=> (map? a)
true
user=> (map? 1)
false
user=> (count a)
3

type は引数のデータ型 (Java のクラス) を調べる関数です。Clojure はカンマ ( , ) を空白文字として扱います。REPL では、キーと値を組にしてカンマで区切って表示します。データの入力やプログラムでもカンマを使うことができます。

Array Map は入力したデータの順番を保持します。関数 first を適用すると、先頭のペア (キーと値を格納したベクタ) を求めることができます。データ入力の順番によって first の返り値は異なります。ただし、マップの中身 (キーと値) が同じであれば、述語 = で比較すると true を返します。データ型のチェックは型述語 map? を使います。マップの大きさは関数 count で求めることができます。

user=> (def b (hash-map :foo 10 :bar 20 :baz 30))
#'user/b
user=> b
{:baz 30, :bar 20, :foo 10}
user=> (type b)
clojure.lang.PersistentHashMap
user=> (= a b)
true

user=> (def c (sorted-map :foo 10 :bar 20 :baz 30))
#'user/c
user=> c
{:bar 20, :baz 30, :foo 10}
user=> (type c)
clojure.lang.PersistentTreeMap
user=> (first c)
[:bar 20]
user=> (last c)
[:foo 10]

user=> (sorted-map-by < 10 "foo", 30 "baz", 20 "bar")
{10 "foo", 20 "bar", 30 "baz"}
user=> (sorted-map-by > 10 "foo", 30 "baz", 20 "bar")
{30 "baz", 20 "bar", 10 "foo"}

user=> (= a c)
true
user=> (= b c)
true

関数 hash-map は Hash Map を生成します。REPL で表示されるデータの順番は、ハッシュ法の実装アルゴリズムに依存します。関数 sorted-map は Sorted Map を生成します。Sorted Map はキーを基準にソートした状態を保ちます。first で最小のペアを、last で最大のペアを求めることができます。デフォルトの比較関数は compare です。比較関数を変更する場合は sorted-map-by を使います。どのマップでも中身 (キーと値) が同じであれば、述語 = は true を返します。

Array Map は単純な実装方法なので、データ数が多くなるとキーの探索速度は遅くなります。Array Map で実用的なデータ数は一桁程度のようです。実際、Array Map にデータを追加したとき、8 個を超えると Hash Map に変換されます。

●マップの基本的な操作

キーに対応する値を求めるには関数 get を使います。

マップ map にキー key があれば、その値を返します。キーが見つからない場合は nil を返します。第 3 引数に not-found を指定すると、キーが見つからない場合は not-found を返します。

user=> a
{:foo 10, :bar 20, :baz 30}
user=> (get a :foo)
10
user=> (get a :oops)
nil
user=> (get a :oops -1)
-1

また、マップは関数のように呼び出すことができます。(map key) を評価すると、key の値を求めることができます。関数 contains? はマップにキーが含まれていると true を返します。関数 find はマップからキーを探し、見つけた場合はペア (キーと値を格納したベクタ) を返します。

user=> a
{:foo 10, :bar 20, :baz 30}
user=> (a :bar)
20
user=> (a :oops)
nil
user=> (a :oops -1)
-1

user=> (contains? a :baz)
true
user=> (contains? a :oops)
false

user=> (find a :foo)
[:foo 10]
user=> (find a :oops)
nil

●データの追加と削除

データの追加は関数 assoc を使います。

user=> a
{:foo 10, :bar 20, :baz 30}
user=> (def d (assoc a :oops 40))
#'user/d
user=> d
{:foo 10, :bar 20, :baz 30, :oops 40}
user=> (get d :oops)
40
user=> a
{:foo 10, :bar 20, :baz 30}

user=> (assoc a :baz 100)
{:foo 10, :bar 20, :baz 100}

(assoc a :oops 40) は、マップ a に :oops と 40 を追加した新しいマップを生成します。キー key が存在している場合は、その値を value に置き換えた新しいマップを生成して返します。

マップは conj を使ってもデータを追加することができます。

引数にキーと値のペアを渡すところが異なるだけで、あとの動作は assoc と同じです。

user=> a
{:foo 10, :bar 20, :baz 30}
user=> (conj a [:oops 40])
{:foo 10, :bar 20, :baz 30, :oops 40}
user=> (conj a [:oops 40] [:baz 50])
{:foo 10, :bar 20, :baz 50, :oops 40}
user=> (conj a [:foo 100] [:baz 50])
{:foo 100, :bar 20, :baz 50}

データの削除は関数 dissoc で行います。

user=> a
{:foo 10, :bar 20, :baz 30}

user=> (dissoc a :foo)
{:bar 20, :baz 30}
user=> (dissoc a :foo :baz)
{:bar 20}
user=> (dissoc a :oops)
{:foo 10, :bar 20, :baz 30}

user=> (identical? a (dissoc a :oops))
true

dissoc は引数の map からキーとその値を削除した新しいマップを生成して返します。キーが見つからない場合は、引数の map をそのまま返します。

●データの更新

Clojure のマップは immutable ですが、キーや値を更新した新しいマップを生成することはできます。

関数 update は key の値を val とすると、(func val args ...) の評価結果が新しい値になります。update-vals は map の値を val とすると、それを (func val) の評価結果に置き換えた新しいマップを返します。

user=> a
{:foo 10, :bar 20, :baz 30}

user=> (update a :foo inc)
{:foo 11, :bar 20, :baz 30}
user=> (update a :foo + 10)
{:foo 20, :bar 20, :baz 30}

user=> (update-vals a dec)
{:foo 9, :bar 19, :baz 29}
user=> (update-vals a #(- % 10))
{:foo 0, :bar 10, :baz 20}

update-keys は map のキーを key とすると、それを (func key) の評価結果に置き換えた新しいマップを返します。

user=> (symbol :foo)
foo
user=> (update-keys a symbol)
{foo 10, bar 20, baz 30}

関数 symbol は引数をシンボルに変換します。

●マップの操作関数

関数 keys は引数 map のキーをシーケンスに格納して返します。関数 vals は map の値をシーケンスに格納して返します。関数 zipmap はキーを格納したシーケンス ks と値を格納したシーケンス vs からマップを生成します。merge は複数のマップを併合して一つのマップを生成します。関数 seq は引数 coll をシーケンスに変換します。coll がマップの場合、ペア (キーと値を格納したベクタ) を格納したシーケンスを返します。

user=> a
{:foo 10, :bar 20, :baz 30}
user=> (keys a)
(:foo :bar :baz)
user=> (vals a)
(10 20 30)
user=> (zipmap [:a :b :c] [1 2 3])
{:a 1, :b 2, :c 3}

user=> (merge {:a 1 :b 2} {:c 3 :d 4})
{:a 1, :b 2, :c 3, :d 4}
user=> (merge {:a 1 :b 2 :c 3} {:d 4 :e 5 :c 6})
{:a 1, :b 2, :c 6, :d 4, :e 5}
user=> (merge)
nil

user=> (seq a)
([:foo 10] [:bar 20] [:baz 30])

●マップの分配束縛

分配束縛はマップを使っても行うことができます。

[{変数名 キー ...} {キー 値 ...} ...]

左側の { } の中で変数の定義と初期値の指定を行います。初期値の指定にはキーを使います。すると、右側のマップからキーを検索し、その値が変数の初期値となります。キーが見つからない場合、変数は nil に初期化されます。

簡単な実行例を示します。

user=> (let [{a :a b :b} {:a 10 :b 20}] (println a b))
10 20
nil
user=> (let [{a :a b :b c :c} {:a 10 :b 20}] (println a b c))
10 20 nil
nil
user=> a
{:foo 10, :bar 20, :baz 30}
user=> (let [{x :foo y :bar z :baz} a] (println x y z))
10 20 30
nil

キーと同じ名前の変数を定義する場合、次のキーワードを使うと便利です。

簡単な実行例を示します。

user=> (let [{:keys [a b]} {:a 10 :b 20}] (println a b))
10 20
nil
user=> (let [{:strs [a b]} {"a" 10 "b" 20}] (println a b))
10 20
nil
user=> (let [{:syms [a b]} {'a 10 'b 20}] (println a b))
10 20
nil

キーワード :or を使うと変数のデフォルト値を指定することができます。

[{:keys [変数名 ...] :or {変数名 デフォルト値 ...}} ...]

:or の後ろのマップで変数に対応するデフォルト値を指定します。キーが見つからない場合はデフォルト値が使用されます。簡単な実行例を示します。

user=> (let [{:keys [a b c] :or {a 1 b 2 c 3}} {:a 10 :b 20}] (println a b c))
10 20 3
nil
user=> (let [{:keys [a b c] :or {a 1 b 2 c 3}} {:b 20}] (println a b c))
1 20 3
nil
user=> (let [{:keys [a b c] :or {a 1 b 2 c 3}} {}] (println a b c))
1 2 3
nil

それから、マップの分配束縛は入れ子にすることができ、ベクタの分配束縛と混在してもかまいません。

user=> (let [{{:keys [a b]} :x {:keys [c d]} :y} {:x {:a 1 :b 2} :y {:c 3 :d 4}}] 
(println a b c d))
1 2 3 4
nil

user=> (let [[{:keys [a b]} {:keys [c d]}] [{:a 1 :b 2} {:c 3 :d 4}]]
(println a b c d))
1 2 3 4
nil

分配束縛はとても便利な機能です。興味のある方は Clojure のリファレンスマニュアルをお読みくださいませ。

●セットとは?

「セット (Set)」はリストやマップのように要素を格納するコレクションですが、リストと違って要素に順番はなく、重複した要素を含まないところが特徴です。セットはデータの集合を表すのに便利なデータ型です。集合は要素の順番に意味はありません。たとえば集合 {1, 3, 5, 7} は {7, 5, 3, 1} や {5, 3, 1, 7} と表すこともできます。このように、要素は適当に並べてもかまわないのですが、ある規則で要素を整列させておく場合もあります。なお、Clojure のセットは immutable です。

●セットの生成

セットは次の方法で作成します。

セットは #{ ... } の中に要素を空白で区切って記述します。#{ } は要素が一つもない空セットになります。#{ ... } は read で読み込むことができる形式で、プログラムの中で指定することもできます。生成されるセットは Hash Set です。

関数 set はコレクション coll を Hash Set に変換します。hash-set は引数を格納した Hash Set を返します。sorted-set は引数を格納した Sorted Set を返します。Hash Set は実装方法にハッシュ法を、Sorted Set は木構造を用いています。sorted-set-by は引数 comp に比較関数を渡します。

簡単な例を示しましょう。

user=> (def a #{1 2 3 4 5})
#'user/a
user=> a
#{1 4 3 2 5}
user=> #{1 2 3 4 5 1 3 5}
Error
user=> (type a)
clojure.lang.PersistentHashSet

user=> (set '(1 2 3 4 5))
#{1 4 3 2 5}
user=> (set '(1 2 3 4 5 1 3 5))
#{1 4 3 2 5}
user=> (hash-set 1 2 3 4 5)
#{1 4 3 2 5}

(set? a)
true
(set? 1)
false
(count a)
5

#{ ... } でセットを生成する場合、重複要素があるとエラーになります。set, hash-set の場合、重複要素はひとつだけセットに登録されます。データ型のチェックは型述語 set? を使います。マップの大きさは関数 count で求めることができます。

user=> (def b (sorted-set 1 2 3 4 5))
#'user/b
user=> b
#{1 2 3 4 5}
user=> (type b)
clojure.lang.PersistentTreeSet

user=> (first b)
1
user=> (last b)
5
user=> (= a b)
true

user=> (sorted-set-by > 1 2 3 4 5)
#{5 4 3 2 1}
user=> (sorted-set-by < 1 2 3 4 5)
#{1 2 3 4 5}

関数 sorted-set は Sorted Set を生成します。Sorted set は要素をソートした状態に保ちます。first で最小の要素を、last で最大の要素を求めることができます。デフォルトの比較関数は compare です。比較関数を変更する場合は sorted-set-by を使います。どのセットでも全ての要素が同じであれば、述語 = は true を返します。

●セットの基本的な操作

セットの要素を取得するには関数 get を使います。

セット s に要素 item があれば、それ返します。item が見つからない場合は nil を返します。第 3 引数に not-found を指定すると、item が見つからない場合は not-found を返します。

user=> a
#{1 4 3 2 5}
user=> (get a 1)
1
user=> (get a 10)
nil
user=> (get a 10 -1)
-1

また、セットは関数のように呼び出すことができます。(set item) を評価すると、item を求めることができます。ただし、第 2 引数に not-found を指定することはできません。関数 contains? はセットに item が含まれていると true を返します。

user=> (a 1)
1
user=> (a 10)
nil
user=> (a 10 -1)
Error
user=> (contains? a 1)
true
user=> (contains? a 10)
false

●データの追加と削除

データの追加は conj で、削除は disj で行います。

conj は set に引数の item を追加します。重複要素がある場合は追加されません。disj は set から引数の item を削除します。item が見つからない場合は無視されます。

user=> a
#{1 4 3 2 5}
user=> (conj a 6 7 8)
#{7 1 4 6 3 2 5 8}
user=> (conj a 1 3 5)
#{1 4 3 2 5}

user=> (disj a 1 3 5)
#{4 2}
user=> (disj a 6 7 8)
#{1 4 3 2 5}

●集合演算

セットは集合を扱う関数が用意されています。主な関数を下表に示します。

表 : セットの集合演算
関数機能
union [s ...] 引数の和集合を求める
intersection s [s1 ...}引数の積集合を求める
difference s [s1 ...}s から残りの引数を引いた集合 (差集合) を求める
select pred ss から述語 pred が真を返す要素を求める
subset? s1 s2 s1 が s2 の部分集合であれば真を返す
superset? s1 s2 s1 が s2 の上位集合であれば真を返す

これらの関数を使う場合、Clojure のライブラリ clojure.set をロードします。ライブラリのロードは関数 require で行います。

require 'lib-name
require '[lib-name :as alias]

関数呼び出しは lib-name/func-name とします。:as を使って別名 alias を付けることもできます。ほかにもいろいろな機能があるので、詳細は Clojure のリファレンスマニュアルをお読みください。簡単な例を示しましょう。

user=> (require '[clojure.set :as set])
nil
user=> (def a #{1 2 3 4})
#'user/a
user=> a
#{1 4 3 2}
user=> (def b #{3 4 5 6})
#'user/b
user=> b
#{4 6 3 5}

user=> (set/union a b)
#{1 4 6 3 2 5}
user=> (set/intersection a b)
#{4 3}
user=> (set/difference a b)
#{1 2}
user=> (set/difference b a)
#{6 5}
user=> (set/select odd? a)
#{1 3}

user=> (set/subset? #{1 2} a)
true
user=> (set/subset? #{3 4} a)
true
user=> (set/subset? #{5 6} a)
false
user=> (set/superset? a #{2 3})
true
user=> (set/superset? a #{5})
false

●コレクションの操作関数

一般に、複数の要素を格納するデータ型を「コンテナ (container)」とか「コレクション (collection)」と呼びます。これまでに説明したリスト、ベクタ、マップ、セットがコレクションになります。Clojure はこれらのデータ型を操作する共通な関数が用意されています。今までにも、count, empty?, conj などを使ってきましたが、ここで他の関数も簡単に説明しておきましょう。

doseq と for はコレクションでも動作します。マップとセットは seq を適用した結果と同じになります。簡単な例を示しましょう。

user=> (doseq [x '(1 2 3 4)] (println x))
1
2
3
4
nil
user=> (doseq [x [1 2 3 4]] (println x))
1
2
3
4
nil
user=> (doseq [x {1 2 3 4}] (println x))
[1 2]
[3 4]
nil
user=> (doseq [x #{1 2 3 4}] (println x))
1
4
3
2
nil

user=> (for [x '(1 2 3 4)] x)
(1 2 3 4)
user=> (for [x [1 2 3 4]] x)
(1 2 3 4)
user=> (for [x {1 2 3 4}] x)
([1 2] [3 4])
user=> (for [x #{1 2 3 4}] x)
(1 4 3 2)

apply は最後の引数がコレクションでも動作します。この場合も最後の引数に seq を適用した結果と同じになります。

user=> (apply list '(1 2 3 4))
(1 2 3 4)
user=> (apply list [1 2 3 4])
(1 2 3 4)
user=> (apply list {1 2 3 4})
([1 2] [3 4])
user=> (apply list #{1 2 3 4})
(1 4 3 2)

into はコレクション from の要素をコレクション to に追加した新しいコレクションを返します。生成されるコレクションの型は to と同じです。基本的には次の S 式と同じ動作になります。

(into to from) ≡ (apply conj to from)
user=> (into '(1 2 3) [4 5 6])
(6 5 4 1 2 3)
user=> (into [1 2 3] [4 5 6])
[1 2 3 4 5 6]
user=> (into #{1 2 3} [4 5 6])
#{1 4 6 3 2 5}
user=> (into {1 2 3 4} [[5 6] [7 8]])
{1 2, 3 4, 5 6, 7 8}

(info to xform from) は (info to (xform from)) と同じです。つまり、form を xform で変換した結果を to に追加します。Clojure の場合、map, filter, remove など高階関数は、カリー化関数のような使い方が可能です。たとえば、(map #(% %)) を評価すれば、リストの要素を 2 乗する関数を返します。

user=>: (into '(1 2 3) (map #(* % %)) [4 5 6])
(36 25 16 1 2 3)
user=> (into [1 2 3] (filter even?) [4 5 6])
[1 2 3 4 6]
user=>: (into #{1 2 3} (remove even?) [4 5 6 7])
#{7 1 3 2 5}
user=>: (into {1 2 3 4} (map (fn [[x y]] [x (* y 2)])) [[5 6] [7 8]])
{1 2, 3 4, 5 12, 7 16}

every? はコレクション coll の要素に述語 pred を適用し、一つでも偽を返す要素があれば偽を返します。すべての要素が真を返す場合、every? は真を返します。not-every? は (not (every? ...)) と同じです。some はリストの要素に述語 pred を適用し、ひとつでも真を返す要素があれば真を返します。すべての要素が偽を返す場合、some は偽を返します。not-any? は (not (some ...)) と同じです。

user=> (every? odd? '(1 3 5 7 9))
true
user=> (every? odd? '(1 2 3 5 7 9))
false
user=> (not-every? odd? '(1 3 5 7 9))
false
user=> (not-every? odd? '(1 2 3 5 7 9))
true

user=> (some even? '(1 2 3 5 7 9))
true
user=> (some even? '(1 3 5 7 9))
nil
user=> (not-any? even? '(1 2 3 5 7 9))
false
user=> (not-any? even? '(1 3 5 7 9))
true

user=> (every? (fn [[_ v]] (odd? v)) {:foo 1 :bar 3 :baz 5})
true
user=> (every? (fn [[_ v]] (odd? v)) {:foo 1 :bar 3 :baz 5 :oops 0})
false

distinct? は引数に重複がなければ true を返し、重複があれば false を返します。distinct は引数 coll から重複要素を取り除いた新しいコレクションを返します。

user=> (distinct? 1)
true
user=> (distinct? 1 2 3 4 5)
true
user=> (distinct? 1 2 3 4 5 1)
false

user=> (apply distinct? '(1 2 3 4 5))
true
user=> (apply distinct? [1 2 3 4 5 1])
false

user=> (distinct '(1 2 3 4 5))
(1 2 3 4 5)
user=> (distinct '(1 2 3 4 5 1 2 3))
(1 2 3 4 5)
user=> (distinct [1 2 3 4 5 3 2 1])
(1 2 3 4 5)
user=> (distinct [1 2 3 4 5 5 5 5])
(1 2 3 4 5)

●簡単な例題

それでは簡単な例題として、3 次元空間の異なる点 [x y z] を n 個作る関数を作ります。要素 x, y, z は 0 から 99 までの整数値とし、乱数で生成することにします。ここでコンピュータで扱う乱数について簡単に説明しておきましょう。

私たちが適当な数字を決める場合、たとえばサイコロを使えば、1 から 6 までの数字を簡単に決めることができます。サイコロを振って出た目を記録したら、次のようになったとしましょう。

5, 2, 1, 2, 6, 3, 4, 3, 1, 5, .....

サイコロの目は、イカサマをしないかぎり、出る確率が 1/6 で規則性はまったくありません。いま 2 が出たから次は 1 が出るとか、3, 4 と続いたから次は 5 が出るといったように、前に出た数字から次に出る数字を予測することはできないのです。このように、でたらめに並んだ数列を「乱数列 (random sequence)」といい、乱数列の中のひとつひとつつの数字を「乱数 (random numbers)」といいます。

コンピュータは、決められた手順 (プログラム) を高速に実行することは得意ですが、まったくでたらめな数を作れといわれると、とたんに困ってしまいます。そこで、何かしらの数式をプログラムして、それを実行することで乱数を発生させます。厳密にいえば乱数ではありませんが、それを乱数としてみなして使うことにするのです。このような乱数を「疑似乱数 (pseudo-random numbers)」といいます。現在では、ほとんどのコンピュータが疑似乱数を使っています。

Clojure には乱数を生成する関数 rand と rand-int が用意されています。

rand は 0 以上 n 未満の乱数を浮動小数点数 (Double) で返します。n のデフォルト値は 1 です。rand-int は 0 以上 n 未満の整数値 (Long) を返します。簡単な例を示しましょう。

user=> (dotimes [_ 10] (println (rand)))
0.6811675639454468
0.18540296388881838
0.8650502047810206
0.8588169722883902
0.09849136371175271
0.6379084710720743
0.8042971913084749
0.808706734448539
0.22518763556343524
0.6962755054741891
nil

user=> (dotimes [_ 10] (println (rand-int 100)))
2
95
57
72
53
40
51
77
65
96
nil

生成する点の個数が少なければ、マップやセットを使わなくても、ベクタを線形探索するだけで十分です。プログラムは次のようになります。

リスト : n 個の異なる点を作る

;; 点 [x y z] を作る
(defn make-point []
  [(rand-int 100) (rand-int 100) (rand-int 100)])

;; 異なる点を N 個作る
(defn make-data [n]
  (loop [n n zs []]
    (if (zero? n)
      zs
      (let [xs (make-point)]
        (if (neg? (.indexOf zs xs))
          (recur (dec n) (conj zs xs))
          (recur n zs))))))

関数 make-data は関数 make-point で点をひとつ生成し、それが今まで生成した点と異なることを .indexOf でチェックします。.indexOf の比較関数は compare です。.indexOf は線形探索なので、点の個数が増えると時間がかかるようになります。実際に 5000, 10000, 20000 個の点を作ったときの実行時間を示します。

表 : 実行時間 (単位 : 秒)
50001000020000
線形探索0.601.706.16

点の個数が増えると実行時間が大幅に増加することがわかります。それでは線形探索の代わりにセットを使ってみましょう。プログラムは次のようになります。

リスト : n 個の異なる点を作る (セット)

;; 異なる点を N 個作る
(defn make-data-fast [n]
  (loop [n n zs (hash-set)]
    (if (zero? n)
      zs
      (let [xs (make-point)]
        (if (contains? zs xs)
          (recur n zs)
          (recur (dec n) (conj zs xs)))))))

(defn make-data-fast1 [n]
  (loop [n n zs (sorted-set)]
    (if (zero? n)
      zs
      (let [xs (make-point)]
        (if (contains? zs xs)
          (recur n zs)
          (recur (dec n) (conj zs xs)))))))

関数 make-data-fast は生成したデータのチェックに Hash Set を使います。セットは累積変数 zs に用意します。そして、make-point でデータを生成したら、contains? で同一データがないかチェックします。contains? の返り値が偽ならば xs は新しいデータです。セット zs に xs を追加して loop / recur で繰り返します。make-data-fast1 は Hash Set のかわりに Sorted Set を使用したものです。

これでプログラムは完成です。実行時間は次のようになりました。

表 : 実行時間 (単位 : 秒)
50001000020000
線形探索 0.601.706.16
Hash Set 0.0080.0160.023
Sorted Set0.0120.0410.046

線形探索と比べて圧倒的にセットの方が速いですね。Hash Set と Sorted Set では、ハッシュ法を使った Hash Set の方が少し速いようです。興味のある方はいろいろ試してみてください。

●まとめ

  1. マップ (Map) は連想配列、セット (Set) は集合を表すデータ型
  2. マップは { }, array-map, hash-map, sorted-map, sorted-map-by で生成する
  3. セットは #{ }, hash-set, sorted-set で生成する
  4. マップとセットから要素を得るには関数 get を使う
  5. マップとセットを関数のように呼び出すと、get のように動作する
  6. contains? はデータの有無をチェックする述語 (マップはキー、セットは要素)
  7. データの追加: マップは assoc, conj, セットは conj を使う
  8. データの削除: マップは dissoc, セットは disj を使う
  9. マップにはデータを更新する update-map, update-vals, update-keys がある
  10. マップのキーは keys, 値は vals で求めることができる
  11. マップを使って分配束縛を行うことができる
  12. セットには集合演算を行う union, interseciton, difference などがある
  13. マップとセットは doseq, for, apply でも動作する
  14. コレクションにはデータを追加する関数 into がある
  15. コレクションには every?, not-every?, some?, not-any という述語がある
  16. distinct? は引数に重複要素が無ければ真を返す
  17. distinct は引数のコレクションから重複要素を削除する

初版 2025 年 5 月 22 日