Enumerable#grep などでも when 節のように複数のパターンを指定したい

case 式で `when 10..20, -20..-10` とはできるのに、 `Enumerable#grep` で `ary.grep(10..20, -20..-10)` とはできない。これをどうにかしてみたいお話（小ネタ）。

Ruby バージョン： 3.1.2

## 背景
Ruby では case 式の条件分岐に使われる `===` という演算子がある。通常は `==` と同じだが、いくつかのクラスでは**条件分岐が便利になるよう再定義**されている。

```ruby
def judge(obj)
	case obj
	when true, false
		puts "%p is a boolean object." % obj
	when 10..20, -20..-10
		puts "abs(%p) is between 10 and 20." % obj
	when /e/
		puts "%p contains a character 'e'." % obj
	when Exception
		puts "%p is an instance of Exception." % obj
	when :frozen?.to_proc
		puts "%p is frozen." % obj
	else
		puts "%p did not match the defined patterns." % obj
	end
end

[
	false,
	-12,
	:Antares,
	(Math.log(-1) rescue $!),
	-"Antarctica",
	self,
].each { |obj| judge(obj) }
```

```text:output
false is a boolean object.
abs(-12) is between 10 and 20.
:Antares contains a character 'e'.
#<Math::DomainError: Numerical argument is out of domain - log> is an instance of Exception.
"Antarctica" is frozen.
main did not match the defined patterns.
```

また、 `Enumerable#grep` や `Enumerable#all?` などでも同じく `===` で判定していて、**ブロックを書かず楽に条件を指定できる**ようになっている。

```ruby
ary = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

# パターンを指定する方法
ary.grep(10..20)                    # Range を指定
ary.grep((10..20).method(:cover?))  # Method を指定
#=> [11, 13, 17, 19]

# ブロックを渡す方法 ※ grep に相当するメソッドは select
ary.select { |x| (10..20).cover?(x) }
ary.select(&(10..20).method(:cover?))
#=> [11, 13, 17, 19]
```

しかし、これらのメソッドで渡せるパターンは1つのみ。 when 節のように複数は書けない。

```ruby
(-100..100).step(7).grep(10..20, -20..-10)
# wrong number of arguments (given 2, expected 1) (ArgumentError)
```

~~特段困った経験は無いが、~~ `===` で判定するという共通点からすれば複数書けてもいい気がする。

## 解決案
引数にパターンをひとつしか渡せないのなら、「内部で複数のパターンを評価するオブジェクト」を作って渡してしまえばいい。具体的には、例えば以下のように使えるクラス `Patterns` を定義する。

```ruby
(-100..100).step(7).grep(Patterns.new(10..20, -20..-10))
#=> [-16, 12, 19]
```

このクラスは2つの要件を満たせばいい。

* コンストラクタで任意の数のパターンを受け取って記憶する
* インスタンスメソッド `#===` を再定義し、それらのパターンを順次評価する
	* 各パターンの評価には再び `===` を用いる
	* 結果を集計して真偽値を返す

when 節だと「複数パターンのいずれか」と評価するので、同様の評価をさせるには `Enumerable#any?` を使うと便利。（今回は欲張りに `#any?` 以外も選べるようにしてみる）

```ruby
class Patterns
	def initialize(*patterns, cond: :any?)
		@patterns = patterns.method(cond)
	end

	def ===(obj)
		@patterns.call { |pattern| pattern === obj }
	end
end
```

### 副次効果
`cond` というオプションで評価方法を選べるようにしたことで、別の使い道が生まれる。

複数パターンの評価方法を `#all?` に変えれば、今度は case 式で「すべての条件を満たす場合」という書き方もできるようになる。

```ruby
multiple_of_3 = -> x { x % 3 == 0 }
multiple_of_5 = -> x { x % 5 == 0 }

(1..100).each do |x|
	puts case x
	when Patterns.new(multiple_of_3, multiple_of_5, cond: :all?)
		"FizzBuzz"
	when multiple_of_3
		"Fizz"
	when multiple_of_5
		"Buzz"
	else
		x
	end
end
```

また `#none?` を使えば否定も作れるし、入れ子にして複雑な条件も表せる。実行速度はともかくとして、色々な使い方ができるかもしれない。

## 付録
### `#===` を再定義しているクラス
デフォルトでは同値性判定になっている。

https://docs.ruby-lang.org/ja/3.1/method/Object/i/=3d=3d=3d.html

> メソッド `Object#==` の別名です。 case 式で使用されます。このメソッドは case 式での振る舞いを考慮して、各クラスの性質に合わせて再定義すべきです。
> 
> 一般的に所属性のチェックを実現するため適宜再定義されます。

---

所属性判定などの形に再定義しているのは、組み込みや標準添付ライブラリをざっと探すと以下があった。他のメソッドのエイリアスに過ぎないものと、固有の判定になっているものがあるため、詳細は各ドキュメントを参照。

| クラス | `#===` の判定基準 | 類似の方法 |
|:--|:--|:--|
| [Gem::Requirement](https://docs.ruby-lang.org/ja/3.1/method/Gem=3a=3aRequirement/i/=3d=3d=3d.html) | バージョン指定を満たすか | `req.satisfied_by?(obj)` |
| [IPAddr](https://docs.ruby-lang.org/ja/3.1/method/IPAddr/i/=3d=3d=3d.html) | IPアドレス範囲に含まれるか | `ip.include?(obj)` |
| [Method](https://docs.ruby-lang.org/ja/3.1/method/Method/i/=3d=3d=3d.html) | （あるオブジェクトをレシーバーとする）メソッドに引数を渡して評価 | `method.call(obj)` |
| [Module](https://docs.ruby-lang.org/ja/3.1/method/Module/i/=3d=3d=3d.html) | そのクラス（継承・ mix-in を含む）のインスタンスか | `obj.kind_of?(mdl)` |
| [Proc](https://docs.ruby-lang.org/ja/3.1/method/Proc/i/=3d=3d=3d.html) | プロシージャに引数を渡して評価 | `proc.call(obj)` |
| [Range](https://docs.ruby-lang.org/ja/3.1/method/Range/i/=3d=3d=3d.html) | オブジェクトが区間に属するか | `range.cover?(obj)` |
| [Regexp](https://docs.ruby-lang.org/ja/3.1/method/Regexp/i/=3d=3d=3d.html) | 文字列が正規表現にマッチするか | `regexp.match?(obj)` |
| [Set](https://docs.ruby-lang.org/ja/3.1/method/Set/i/=3d=3d=3d.html) | オブジェクトが集合に属するか | `set.member?(obj)` |

### `#===` を利用するメソッド
主なものは `Enumerable` にある。このモジュールを mix-in しているリスト的なクラスで利用できる。

https://docs.ruby-lang.org/ja/3.1/class/Enumerable.html

* `#grep`, `#grep_v` ： 要素の抽出
	* パターン指定が必須
	* ブロックを追加した場合、要素を変換して返す（ `#map` と同様）
* `#any?`, `#all?`, `#none?`, `#one?` ： 統計的判定
	* パターン、ブロック、何もなし（要素のまま評価）を選べる
* `#slice_after`, `#slice_before` ： リストの分割
	* パターンとブロックを選べる