なぜこの記事を書いたのか
※追記※@scivola様に内容のご指摘をいただきましたので修正します。ありがとうございます🙇♂
業務中に簡単なコードを書こうとして「配列Aと配列Bで共通の要素を抽出できないか」ということを調べたときに、このSet
クラスの存在を初めて知りましたが、結構便利そうなので興味本位で調べてみることにしました。
※自分がまじで低レベルなので鬼基本的な内容です(間違っていたらご指摘ください🙇♂)
Setクラス。。?
- Ruby3.1までは標準ライブラリのため
require('set')
が必要 - Ruby3.2からは組み込みになったため
require('set')
は不要 - class Set (Ruby 3.3 リファレンスマニュアル)から引用すると最初に以下のような説明がある👀
集合を表すクラスです。要素の間に順序関係はありません。
ちょっとこれだけだとピンとはこない。。。
- ただ、
集合
と順序関係
がちょっとしたキーワードになりそう👀 - 以下でRubyにおける集合に関して、そしてsetクラスに関してをざっくり書いていきます
Rubyにおける集合
本質的な難しい話は置いておいて(できんし)ざっくり集合とは、、、
-
重複する要素を持たない一意な要素のコレクション(複数の要素を一つにまとめたデータ構造) を指す
- Rubyでは配列で和集合(
|
)・差集合(-
)・積集合(&
)といった集合を扱うことができる- ちなみに非破壊的(元の配列を壊さない)
- 同様に、RubyではSetクラスでもこの集合を扱うことができる
- Rubyでは配列で和集合(
- 和集合(
|
)
- 2つの配列の要素を1つの配列に集め、かつ重複しないようにしている
配列の場合
irb(main):010> a = [1, 2, 3]
=> [1, 2, 3]
irb(main):011> b = [3, 4, 5]
=> [3, 4, 5]
# 1つの配列に集まり、かつ3が重複していない
irb(main):012> a | b
=> [1, 2, 3, 4, 5]
Setクラスの場合
irb(main):015> a = Set[1, 2, 3]
=> #<Set: {1, 2, 3}>
irb(main):016> b = Set[3, 4, 5]
=> #<Set: {3, 4, 5}>
irb(main):017> a | b
=> #<Set: {1, 2, 3, 4, 5}>
2 差集合(-
)
- 左の配列に含まれる要素と右の配列に含まれる要素が合致する場合、それを取り除く
配列の場合
irb(main):010> a = [1, 2, 3]
=> [1, 2, 3]
irb(main):011> b = [3, 4, 5]
=> [3, 4, 5]
# 3が取り除かれる
irb(main):013> a - b
=> [1, 2]
Setクラスの場合
irb(main):015> a = Set[1, 2, 3]
=> #<Set: {1, 2, 3}>
irb(main):016> b = Set[3, 4, 5]
=> #<Set: {3, 4, 5}>
irb(main):018> a - b
=> #<Set: {1, 2}>
3 積集合(&
)
- 2つの配列に含まれている共通の要素を返す
配列の場合
irb(main):010> a = [1, 2, 3]
=> [1, 2, 3]
irb(main):011> b = [3, 4, 5]
=> [3, 4, 5]
# 共通している3のみ返る
irb(main):014> a & b
=> [3]
Setクラスの場合
irb(main):010> a = [1, 2, 3]
=> [1, 2, 3]
irb(main):011> b = [3, 4, 5]
=> [3, 4, 5]
irb(main):019> a & b
=> #<Set: {3}>
鬼ざっくり、Setクラスでは集合が扱えるのかーーということがわかった
- 集合に関してはこのrubyのArrayで集合(和集合・積集合・差集合・対称差集合・補集合) #Ruby - Qiitaを参考にさせてもらいました🙇♂
Setクラスをもう少しだけ詳しく
1️⃣ Set
クラスに配列を与えることで、新しい集合(setオブジェクト)を作成できる
- 主に以下2つのやり方で作成できる
Set[1, 2] #=> #<Set: {1, 2}>
-
Set.new([1, 2]) #=> #<Set: {1, 2}>
Set[]
というように少し特殊な書き方だが、これはSet.new([])
のシンタックスシュガーのため、結果はどちらも同じ- すいませんシンタックスシュガーと書きましたが間違いでした、、、、
Set[]
とSet.new
はSetオブジェクトを作るための全く別の手段 - 以下でいただいたご指摘をもとに内容を修正・加筆
# どちらも結果は同じsetオブジェクト(集合)を作成
irb(main):024> Set[1, 2]
=> #<Set: {1, 2}>
irb(main):026> Set.new([1, 2])
=> #<Set: {1, 2}>
Set[]
-
Set.[] (Ruby 3.3 リファレンスマニュアル)
- 与えられたオブジェクトを要素とする新しい集合を作る
-
[]
には集合の要素としたいものを引数として指定する -
[]
に引数を渡さない場合、空のsetオブジェクトが作成される
irb(main):003> Set[]
=> #<Set: {}>
-
Set[]
はSetクラスをレシーバーとして[]
というメソッドを呼び出すことの特殊記法-
[]
は Setのクラスメソッド(特異メソッド) -
class Set (Ruby 3.3 リファレンスマニュアル)
-
[]
メソッドの中には、メソッドの実引数として0個以上の式を列挙し、それらがSet
オブジェクトの要素となる
-
-
例
Set[1, 2]
というのは
- レシーバー:
Set
- メソッド:
[]
- 引数:1 および 2
- という意味のメソッド呼び出しになる
- 全く同じ意味のコードとして以下のようになる
Set.[](1, 2)
irb(main):001> Set[1, 2]
=> #<Set: {1, 2}>
irb(main):002> Set.[](1, 2)
=> #<Set: {1, 2}>
Set.new
- Set.new (Ruby 3.3 リファレンスマニュアル)
- 引数で与えられた要素を元に、新しい集合を作る
- 引数を指定しない場合、または引数がnilである場合には、空の集合を作る
irb(main):005> Set.new
=> #<Set: {}>
-
Set.new
は引数を与える場合 eachメソッドを持つオブジェクトを一つだけ与えるという点が、Set[]
との大きな違い-
each
を使ってそのオブジェクトから要素を一つずつ取り出し、それらを要素とするSetオブジェクトを生成
-
例(リファレンス例そのままだが。。。)
irb(main):006> Set.new([1, 2]) {|o| o * 2}
=> #<Set: {2, 4}>
2️⃣ 要素の重複の存在が許容されない
- 集合は重複する要素を持たない特性があるため、setオブジェクトを作成する際に重複があると排除される
irb(main):031> set = Set['aaa', 'aaa']
=> #<Set: {"aaa"}>
irb(main):032> set = Set[1, 1]
=> #<Set: {1}>
# 要素が別属性の場合は削除されない
irb(main):033> set = Set[1, '1']
=> #<Set: {1, "1"}>
3️⃣要素間の順序関係がない
- Setは
内部的にハッシュを使って実装されているらしく要素の順序は保証されない という特性を持つ-
ハッシュ自体もキーの順序を保証しないため、セットも同様に順序関係を持たない- 十何年か前のRuby1.8の頃まではハッシュも順序を持たなかったらしいが、現在のRubyのハッシュは要素の順序を持つ
- 内部的にハッシュが云々という話は関係なく、数学の「集合」という概念が順序を持たないものになっているため
- 配列は順序が保証されるため、ここが大きな違い
-
- 順序関係がないことを証明するために、2つの同要素を持つsetオブジェクトを順不同で作成し、同一かどうかを検証する
irb(main):034> set = Set[3, 1, 2]
=> #<Set: {3, 1, 2}>
irb(main):035> set2 = Set[2, 3, 1]
=> #<Set: {2, 3, 1}>
# 等しいオブジェクト
irb(main):036> puts set == set2
true
配列は順序が保証される
irb(main):037> a = [1, 2, 3]
=> [1, 2, 3]
irb(main):038> b = [2, 3, 1]
=> [2, 3, 1]
irb(main):039> puts a == b
false
4️⃣ setオブジェクトを扱うためのインスタンスメソッドが用意されている
-
add
やmap!
など、Arrayクラスと似たようなメソッドが多い印象 - 当然↑の方で出した
|
や&
もここに含まれる
例) map!
を使ってみる(ブロックの中の処理に応じた新しいsetオブジェクトに作り変える)
irb(main):029> set = Set['hello', 'world']
=> #<Set: {"hello", "world"}>
# 先頭を大文字にしたsetオブジェクトを返す
irb(main):030> set.map! {|str| str.capitalize}
=> #<Set: {"Hello", "World"}>
5️⃣ 継承関係は Set < Enumerable < Object < Kernel < BasicObject
となっているため、Enumerable
クラスのメソッドが使用できる
例) enumerableのfind
を使ってみる
irb(main):020> a = Set[1, 2, 3]
=> #<Set: {1, 2, 3}>
irb(main):023> a.find {|i| i == 3}
=> 3
- ちなみにEnumerableのメソッドにはsetオブジェクトを作成するための
to_set
というメソッドも用意されている
irb(main):041> [2, 3, 1].to_set
=> #<Set: {2, 3, 1}>
Setを使うメリット
ちょっと調べきれていないので、他人様の記事を拝借(また自分で調べて書き足します)
- Setクラスはこう使え!要素の存在確認をするならSetクラス - webエンジニアの日常
-
【Ruby】Array#include?を高速化する方法
- 指定要素の存在チェックはArrayよりも早いらしい
重複を許可せず、かつ並び順を気にしなくて良い という場合にはsetオブジェクトを使ったほうがパフォーマンス的にもよいのかもしれない