導入
ユニークリストとは、重複のない列挙型の値(s)だ。
私はJavaScriptでユニークリストを実現するために汚いコードをこねてやっているのを見たことがある。
その時私は言った。
「汝、Setを使うべし」
Setは便利なものだが、だいたい配列で良いとしがちである上に、他にも代替手段があり、さらにはSetが手軽に利用できる環境は限られることから知名度も低いと苦難に満ちすぎている。
が、覚えておくと良いものであるのは間違いない。
ユニークリストとSet
Setは配列と似ているが、常に重複排除される点、そして順序を保証しない点が大きな違いである。
場合によってはパフォーマンスに優れ、少なくとも配列で都度重複排除するよりは速いことが多い。
Rubyで書くと
a = []
a.push(x)
a.uniq!
a.push(y)
a.uniq!
が
a = Set.new
a.add(x)
a.add(y)
になる。
実はRubyだとSet
の意義は相当怪しい。
が、uniq
みたいなメソッドのないJavaScriptの場合はこうかはばつぐんだ。
const a = new Set()
a.add(x)
a.add(y)
if (a.has("foo")) {
// ...
}
配列はincludes()
なのにSet
はhas()
なのは、最近のESのセンスのない部分。1
用途としてはhas()
で含まれるかどうかを確認するか、values()
でイテレータを回すか、
[...set]
のようにして配列にしてJSONにするかだ。
(JSON.stringify
はSet
をシリアライズできない)
ユニークリスト自体はフラグやタグなど有効なものを列挙する場合によく使用する。
微妙な立ち位置になりがちなSet
JavaScriptではSetがとても有用だという話をしたが、JavaScriptでさえも微妙な要素を持っている。
まず前提として、汎用性のある代替手段として連想配列を使う方法がある。
JavaScriptなら
a = {}
a[x] = true
a[y] = true
これでif (a[x])
のような形で判定にも使えるし、Object.keys(a)
でイテレーションも可能。
動的言語らしいスマートさだ。
Rubyでも全く同じコードが動く。
そして、そのままシリアライズできる。
JavaScriptでユニークリストを扱うのにArrayを使ったら「Setを使え」と言うけれど、Objectを使っていたら何も言えないくらいだ。
JavaScriptでのSetは、JSON.parse()
, JSON.stringify()
で扱えない。
シリアライズ対象になる場合、シリアライズするときは配列に変換し、デシリアライズするときは配列から変換するという処理を手で書く必要がある。
実際のところシリアライズ対象になるなら、Setを使う意義はほぼない。2
Rubyの場合はSet
が標準ライブラリでrequire
が必要な上に、配列でも
a |= x
みたいに書けてしまってパフォーマンスも良いので、さっと書くだけなら出番がないし、Hash
の取り回しが良いのであまり使いたい気持ちがわかない。
subset?
, superset?
, disjoint?
あたりを使うならといったところ。
Pythonではsetはかなり使いやすく作られているし、リストやタプルに変換もできる。
演算子でやれる部分集合判定もあったりするので、比較的使い勝手は良いが、Pythonのsetは単にユニークリストにするためよりは数学的要素のために使われている印象。
それでもSetを覚えておこう
このようにSetは「ユニークリストだからSet!」のように思考停止で使うようなものではない。
が、それでも覚えておくと良い。
ときどきここぞという時があるもので、引き出しに入れておくといいコードを書く助けになる。
例えば、JavaScriptで配列から重複排除したくなったとしよう。
JavaScriptでユニークな配列を作る方法は色んな記事で色々解説されているが、実はそんなことをせずとも
ary = [...new Set(ary)]
とするだけで簡単に重複排除できる。しかも速い。
ちなみに、JavaScriptのsetはコンストラクタに配列を渡すと配列の並びを維持し(最初に要素が出てくる位置に保存される)、展開するときもこの順序を維持するので、配列における順序を維持したい場合でも使えるテクニックである。
[...new Set([1, 1, 2, 3, 2, 4, 6, 8, 10, 2, 1, 1])]
// -> [1, 2, 3, 4, 6, 8, 10]
そのほか、権限で複合判定したい場合も、配列なら
if (rights.includes("writer") && rights.incldues("committer"))
オブジェクトなら
if (rights.writer && rights.committer)
とか書くことになるのだが、Setにしておけば
if (new Set(["writer", "committer"]).isSubsetOf(rights))
とか書ける。3
数が少ないうちはオブジェクトにしておいたほうがコンパクトだが、5個くらい要求する場合はSetを使ったほうがきれい。
あと、判定関数化しやすい。
function hasRights(...user_rights) {
return new Set(user_rights).isSubsetOf(rights)
}
このように、Setのことを引き出しに入れておけばプログラミングの幅が広がる。
ぜひ覚えておこう。