1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ユニークリストには配列ではなくSetを使おう

Posted at

導入

ユニークリストとは、重複のない列挙型の値(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()なのにSethas()なのは、最近のESのセンスのない部分。1

用途としてはhas()で含まれるかどうかを確認するか、values()でイテレータを回すか、

[...set]

のようにして配列にしてJSONにするかだ。
(JSON.stringifySetをシリアライズできない)

ユニークリスト自体はフラグやタグなど有効なものを列挙する場合によく使用する。

微妙な立ち位置になりがちな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のことを引き出しに入れておけばプログラミングの幅が広がる。
ぜひ覚えておこう。

  1. Set.prototype.has() はES2015(Set自体がES2015)、 Array.prototype.includes() はES2016

  2. JavaScriptには同じような不遇な事情を抱えたものとしてMapがある。だが、あっちはもっと使いどころがない。JavaScriptの根本的なコンセプトと絶望的に合っていないのだ。

  3. Set.prototype.isSubsetOf() はES2024で追加された非常に新しいメソッド。もともとJavaScriptのSetは重複排除できるコレクションに過ぎず、数学的な集合ではなかったのだ。

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?