【Ruby】Setクラスについてざっくり調べてみた

# なぜこの記事を書いたのか
※追記※@scivola様に[内容のご指摘](https://qiita.com/nittamatama/items/87d5249fa966bb005fd2#comment-ce1a25062d7897c61960)をいただきましたので修正します。ありがとうございます🙇‍♂


業務中に簡単なコードを書こうとして「配列Aと配列Bで共通の要素を抽出できないか」ということを調べたときに、この`Set`クラスの存在を初めて知りましたが、結構便利そうなので興味本位で調べてみることにしました。

※自分がまじで低レベルなので鬼基本的な内容です（間違っていたらご指摘ください🙇‍♂）

# Setクラス。。？
- Ruby3.1までは標準ライブラリのため`require('set')`が必要
- Ruby3.2からは組み込みになったため`require('set')`は不要
    - [Ruby 3.2.0 Released](https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/#:~:text=Set%20is%20now%20available%20as%20a%20builtin%20class%20without%20the%20need)
- [class Set (Ruby 3.3 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/class/Set.html)から引用すると最初に以下のような説明がある👀

> 集合を表すクラスです。要素の間に順序関係はありません。

ちょっとこれだけだとピンとはこない。。。

- ただ、`集合`と`順序関係`がちょっとしたキーワードになりそう👀
- 以下でRubyにおける集合に関して、そしてsetクラスに関してをざっくり書いていきます

## Rubyにおける集合

本質的な難しい話は置いておいて（できんし）ざっくり集合とは、、、
- **重複する要素を持たない一意な要素のコレクション（複数の要素を一つにまとめたデータ構造）** を指す
    - Rubyでは配列で和集合(`|`)・差集合(`-`)・積集合(`&`)といった集合を扱うことができる
        - ちなみに非破壊的（元の配列を壊さない）
    - 同様に、Rubyでは**Setクラスでもこの集合を扱うことができる**

1. 和集合(`|`)

- 2つの配列の要素を1つの配列に集め、かつ**重複しない**ようにしている

配列の場合
```ruby
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クラスの場合
```ruby
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 差集合(`-`)

- 左の配列に含まれる要素と右の配列に含まれる要素が合致する場合、それを取り除く

配列の場合
```ruby
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クラスの場合
```ruby
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つの配列に含まれている共通の要素を返す

配列の場合
```ruby
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クラスの場合
```ruby
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](https://qiita.com/namitop/items/be11c007da456ea95735)を参考にさせてもらいました🙇‍♂

## 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オブジェクトを作るための全く別の手段
        - 以下で[いただいたご指摘](https://qiita.com/nittamatama/items/87d5249fa966bb005fd2#comment-ce1a25062d7897c61960)をもとに内容を修正・加筆

```ruby
# どちらも結果は同じ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 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/3.3/method/Set/s/=5b=5d.html)
    - 与えられたオブジェクトを要素とする新しい集合を作る
    - `[]`には集合の要素としたいものを引数として指定する
    - `[]`に引数を渡さない場合、空のsetオブジェクトが作成される
```ruby
irb(main):003> Set[]
=> #<Set: {}>
```

- `Set[]`はSetクラスをレシーバーとして`[]`というメソッドを呼び出すことの特殊記法
    - `[]`は Setのクラスメソッド(特異メソッド)
    - [class Set (Ruby 3.3 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/3.3/class/Set.html#S_--5B--5D)
        - `[]`メソッドの中には、メソッドの実引数として0個以上の式を列挙し、それらが`Set`オブジェクトの要素となる

例
```ruby
Set[1, 2]
```
というのは

- レシーバー：`Set`
- メソッド：`[]`
- 引数：1 および 2
    - という意味のメソッド呼び出しになる
- 全く同じ意味のコードとして以下のようになる

```ruby
Set.[](1, 2)
```

```ruby
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 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/3.3/method/Set/s/new.html)
- 引数で与えられた要素を元に、新しい集合を作る
- 引数を指定しない場合、または引数がnilである場合には、空の集合を作る

```ruby
irb(main):005> Set.new
=> #<Set: {}>
```

- `Set.new`は引数を与える場合 **eachメソッドを持つオブジェクト**を一つだけ与えるという点が、`Set[]`との大きな違い
    - `each`を使ってそのオブジェクトから要素を一つずつ取り出し、それらを要素とするSetオブジェクトを生成

例(リファレンス例そのままだが。。。)
```ruby
irb(main):006> Set.new([1, 2]) {|o| o * 2}
=> #<Set: {2, 4}>
```


### 2️⃣ 要素の重複の存在が許容されない
- **集合は重複する要素を持たない特性**があるため、setオブジェクトを作成する際に重複があると排除される

```ruby
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のハッシュは要素の順序を持つ
    - 内部的にハッシュが云々という話は関係なく、数学の「集合」という概念が順序を持たないものになっているため
    - 配列は順序が保証されるため、ここが大きな違い
- 順序関係がないことを証明するために、２つの同要素を持つsetオブジェクトを順不同で作成し、同一かどうかを検証する
```ruby
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
```

配列は順序が保証される
```ruby
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クラスと似たようなメソッドが多い印象
- 当然↑の方で出した`|`や`&`もここに含まれる
    - [Setクラスのインスタンスメソッド](https://docs.ruby-lang.org/ja/latest/class/Set.html#:~:text=%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/990312/50cf4a31-3bc2-8b4d-20e2-61a2e0db8849.png)

例) `map!`を使ってみる(ブロックの中の処理に応じた新しいsetオブジェクトに作り変える)
```ruby
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から継承しているメソッド](https://docs.ruby-lang.org/ja/latest/class/Set.html#:~:text=Enumerable%E3%81%8B%E3%82%89%E7%B6%99%E6%89%BF%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
   

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/990312/7fcd0d4c-3140-d250-7a25-2f4bc33d7e94.png)

例) enumerableの`find`を使ってみる
```ruby
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`というメソッドも用意されている

```ruby
irb(main):041> [2, 3, 1].to_set
=> #<Set: {2, 3, 1}>
```

# Setを使うメリット
ちょっと調べきれていないので、他人様の記事を拝借（また自分で調べて書き足します）
- [Setクラスはこう使え！要素の存在確認をするならSetクラス - webエンジニアの日常](https://www.uosansatox.biz/entry/2018/07/26/080000)
- [【Ruby】Array#include?を高速化する方法](https://zenn.dev/hyzsa/articles/20230914-ruby-array-include-vs-set-include)
    - 指定要素の存在チェックはArrayよりも早いらしい

**重複を許可せず、かつ並び順を気にしなくて良い** という場合にはsetオブジェクトを使ったほうがパフォーマンス的にもよいのかもしれない


# 参考資料
- [class Set (Ruby 3.3 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/class/Set.html)
- [rubyのArrayで集合（和集合・積集合・差集合・対称差集合・補集合） #Ruby - Qiita](https://qiita.com/namitop/items/be11c007da456ea95735)