Ruby における ===

# 追記

- 2018-12-25 Ruby 2.6 における Range の仕様変更について追記。

# 概要

case 式で利用されるメソッド。 *Case equality operator* (case等価演算子) とも呼ばれる。

```ruby
case 5
when String #  String === 5 が呼ばれる。
when /foo/  #   /foo/ === 5 が呼ばれる。
when 1..10  # (1..10) === 5 が呼ばれる。
end
```

また、 [`Enumerable`](https://docs.ruby-lang.org/ja/latest/class/Enumerable.html) のメソッドのうち、 `pattern` を引数とするメソッドにも使われる。

```ruby
[1, 2, 3, 4, 5].grep(2..4) # それぞれの要素に対して (2..4) === element が呼ばれる。
```

- 他のプログラミング言語 (JavaScript や PHP 等) にみられるような、厳密等価演算子、およびそれに類するものではない。
- クラスごとに `===` の定義をオーバーライドできる。
- (`===` に限ったことではないが) 演算子の前後を逆にすると、結果が変わる場合があるため注意。
    - `/foo/ === 'foo'` は true だが、 `'foo' === /foo/` は false。
        - 前者は `Regexp#===` を呼び出しているのに対し、後者は `String#===` を呼び出している。
        - Ruby に慣れていないと間違えやすい印象。
    - 基本的に、演算子の前後の値のクラスが異なる場合、前後を逆にするとまず結果が変わる。
        - クラスが同じであっても、 `IPAddr.new('127.0.0.1/8') === IPAddr.new('127.0.0.1/32')` が true で `IPAddr.new('127.0.0.1/32') === IPAddr.new('127.0.0.1/8')`   が false であるように、クラスの `===` の定義によって挙動は様々。
- RuboCop のスタイルガイドでは、 [**=== を直接使わないよう**言及されている](https://github.com/rubocop-hq/ruby-style-guide#no-case-equality)。
    - なお、 [Airbnb のスタイルガイド](https://github.com/airbnb/ruby) では特にそういった言及はない。

# 特徴的な動きをする ===

`when 1..10` のように書けるよう、 === に **包括関係の検査** を充てているものが多い。
※もちろん、クラスによっては別の意味を充てていることもある。

## `Range#===`

### <= 2.5.x

[`Range#include?`](https://docs.ruby-lang.org/ja/2.5.0/method/Range/i/=3d=3d=3d.html) と同等。

```ruby
(1..10) === 5       # => true
(1..10).include?(5) # ↑と同等。
```

### = 2.6.0

`Range#include?` と `Range#cover?` を合わせたような、説明しづらい挙動になった。

該当のコミット: [range.c: === by cover?](https://github.com/ruby/ruby/commit/989e07c0f2fa664a54e52a475c2fcc145f06539d)

- 数値 (として内部的に扱える値) もしくは文字列の Range である場合は、 2.5.x 以前の `Range#===` と変わらない。
- そうでない場合、 [`Range#cover?`](https://docs.ruby-lang.org/ja/latest/method/Range/i/cover=3f.html) が部分的に使われる。
    - 2.6.0 で [Range#cover? は Range オブジェクトを受け付けるようになった](https://github.com/ruby/ruby/commit/9ca738927293df1c7a2a1ed7e2d6cf89527b5438) が、 `Range#===` では受け付けられないまま。

```ruby
# 文字列の Range であれば、 2.5.x 以前のまま。
('b'..'d').cover?('ba') # 2.5.x でも 2.6.0 でも true
('b'..'d') === 'ba'     # 2.5.x でも 2.6.0 でも false

# 2.6.0 で Range#cover? は Range を受け付けるようになったが、 Range#=== では受け付けないまま。
(1..4).cover?(2..3) # 2.5.x では false 、 2.6.0 では true
(1..4) === (2..3)   # 2.5.x でも 2.6.0 でも false
```

従来、数値 (として内部的に扱える値) でも文字列でもない Range の `include?` は [`Enumerable#include?`](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/include=3f.html) を利用していた。
(各要素と `==` で比較し、 true であるものがあるかどうかの検査)
それが [`Range#cover?`](https://docs.ruby-lang.org/ja/latest/method/Range/i/cover=3f.html) (`<=>` による比較) に変わることで、一部のケースでより納得しやすい結果になるようになった。

```ruby
(Date.today..Date.today + 1).cover?(DateTime.now) # 2.5.x でも 2.6.0 でも true
(Date.today..Date.today + 1) === DateTime.now     # 2.5.x では false 、 2.6.0 では true
```

現状、正確な挙動を把握するにはソースを読むしかない。
2.6.0 対応のるりまが待たれる。

## `Regexp#===`

[`Regexp#match(str)`](https://docs.ruby-lang.org/ja/latest/method/Regexp/i/match.html) と [`Regexp#match?(str)`](https://docs.ruby-lang.org/ja/latest/method/Regexp/i/match=3f.html) の中間的な動作をする。
※ `Regexp#match?` は Ruby 2.4 から。

- 返り値は bool (`match?` と同じ)
- 組み込み変数 `$~` などを変更する (`match` と同じ)

```ruby
/foo/ === 'foo' # => true
$~              # => #<MatchData "foo">

/bar/.match('bar') # => #<MatchData "bar">
$~                 # => #<MatchData "bar">

/baz/.match?('baz') # => true
$~                  # => #<MatchData "bar"> ← match? は $~ を変更しない。
```

## `Module#===`

[`Object#is_a?`](https://docs.ruby-lang.org/ja/latest/method/Object/i/kind_of=3f.html) と同等。

```ruby
String === 'foo'    # => true
'foo'.is_a?(String) # ↑と同等。
```

## `Method#===` `Proc#===`

それぞれ [`Method#call(*args)`](https://docs.ruby-lang.org/ja/latest/method/Method/i/=3d=3d=3d.html) [`Proc#call(*args)`](https://docs.ruby-lang.org/ja/latest/method/Proc/i/=3d=3d=3d.html) (メソッド/Proc の実行) と同等。
※ `Method#===` は Ruby 2.5 から。

```ruby
f = ->(v) { v * 10 }

f === 2   # => 20
f.call(2) # ↑と同等。
```

## `Set#===` (Ruby 2.5)

[`Set#include?`]([`Set#include?`](https://docs.ruby-lang.org/ja/2.5.0/method/Set/i/=3d=3d=3d.html)) と同等。

```ruby
require 'set'

Set[1, 2, 3] === 2       # => true
Set[1, 2, 3].include?(2) # ↑と同等。
```

## `IPAddr#===`

[`IPAddr#include?`](https://docs.ruby-lang.org/ja/latest/method/IPAddr/i/=3d=3d=3d.html) と同等。

```ruby
require 'ipaddr'

IPAddr.new('127.0.0.0/8') === '127.0.0.1'       # => true
IPAddr.new('127.0.0.0/8').include?('127.0.0.1') # ↑と同等。
```