Rubyのcase式と===演算子について

前フリ

奥様、知ってらした?Rubycase式ってすっごくパワフルなんですって!単なる同値判定で振り分けるだけじゃなくて、例えばこんなこともできちゃうらしいんですのよ!

case gets.chomp
when /おはよう/
  puts 'おはようございます、お目覚めはいかがですか?'
when /こんにちは/
  puts 'こんにちは、いい天気ですね。'
when /こんばんは/
  puts 'こんばんは、遅くまでお疲れ様です。'
else
  puts '何か御用でしょうか?'
end

あたくし、今までcase式の実力を見くびっていましたわ…!

ラク

どうしてこんなことができるのかというと、リファレンスマニュアルにも書いてあることなのですが、case式は===演算子を使ったif〜elsif〜end式と等価な処理を行うように実装されているからなんですね。

要は、この===演算子がキモなわけです。

/おはよう/ === 'おはようございます'  #=> true

みたいな式が成り立つからこそ、上述のようなcase式が書けちゃうんですね。

===演算子の正体

他の言語を使っていると、ついつい「===演算子は==演算子よりも更に厳密な同値判定演算子でしょ?」なんて思ってしまいますが、ところがどっこい、Rubyの場合はむしろ===演算子のほうが柔軟な場合もあり得るのです。

Object#===の説明を見てみるとわかりますが、基本的にこの===演算子は、サブクラスで再定義されていない限り、単にObject#==を呼び出すだけなんですね。

で、実際に===演算子を再定義している組み込みクラスにどんなものがあるかというと、

Module
kind_of?を呼んでいるだけ
Proc
callの別名
Range
include?を呼んでいるだけ
Regexp
=~matchとほぼ同じ*1

の4つのみとなっており、それぞれ上記のような実装となっています。いずれも「同値判定」とはやや趣の異なる処理ですね。

つまり、少なくとも組み込みクラスの範囲において、===演算子が==演算子よりも厳密な同値判定であるケースは皆無というわけです。Rubyにおける===演算子は、他の言語における===演算子とは全く別物と割り切るべきである、と言えます。

===は非可換な子

もう一つ気をつけるべき点は、これは===演算子に限ったことではないのですが、Rubyではこの辺の二項演算子がほとんどメソッドとして実装されている関係上、その判定がどのように処理されるかはレシーバに依存します。つまり、「どちらが左辺になるかによって結果が変わることがある」のですね。

例えば先ほどの

/おはよう/ === 'おはようございます'  #=> true

は、

'おはようございます' === /おはよう/  #=> false

と書いてしまうと成り立ちません。何故ならば、レシーバ、つまり左辺に位置しているオブジェクトはStringクラスのインスタンスであり、Stringクラスの===演算子は==演算子、それ即ち同値判定だからです。

活用してみよう

case式と===演算子のこのような性質を知っていれば、例えばオブジェクトのクラスによって処理を振り分けるコードも

case obj
when Array
  puts '配列ですね!'
when Hash
  puts 'ハッシュですね!'
when String, Symbol
  puts '文字列かシンボルですね!'
when Numeric
  puts '数値ですね!'
else
  puts 'わかりません!'
end

というように書けますし、例えば時間帯によって処理を振り分けるコードも

case Time.now.hour
when (6..8)
  puts 'おはようございます!'
when (9..11)
  puts '遅刻しませんでしたか!?'
when (12..13)
  puts 'お昼ですね!カレーですか!?'
when (14..16)
  puts 'シエスタである。邪魔をしてはいけない。'
when (17..18)
  puts 'お疲れ様です!さっさと帰りましょう!'
when (19..20)
  puts '夕飯はカレーですね!'
when (21..23)
  puts 'さっさと歯磨いて寝やがってください!'
when (0..5)
  puts '深夜アニメ好きですね!'
end

みたいに書けます。if〜elsif〜endで書くよりもだいぶすっきり書けますし、意味もわかりやすいですね。

更なる活用

独自に===演算子を定義してやれば、さらにパワフルな振り分けだってできてしまいます。

ただし、その場合に注意しなければならないのは、「case式ではwhen節に指定されたオブジェクトをレシーバとして===メソッドが呼び出される」という点です。

つまり、

case foo
when bar
  puts 'bingo!'
else
  puts 'oops.'
end

というコードは

if bar === foo
  puts 'bingo!'
else
  puts 'oops.'
end

と同義です。

前述の通り===演算子は非可換ですので、この場合はfooオブジェクトではなく、barオブジェクトのクラスに===演算子を定義してやる必要があります。

ついでに==演算子

せっかくなので==演算子の正体も調べてみましょう。

この==演算子という奴は、言語によって同値判定だったり同一判定だったりしますが、Rubyにおける==演算子の機能は…これもまた===演算子と同じく、「レシーバとなるオブジェクトのクラスに依存する」ことになります。というのも、==演算子は===演算子以上に各クラスで詳細に定義されているんですね。

例えば、一番大元となるObject#==は、C言語レベルでのポインタの比較、つまり完全な同一判定です。しかし実際は殆どのクラスで独自に再定義されていますので、Object#==が使用されるケースはそう多くはありません。

それぞれの==演算子が実際に何をやっているかはリファレンスマニュアル(もしくはソースコード)を見てみないとわかりませんが、大抵の場合は

[ 1, 2, 3 ] == [ 1, 2, 3 ]  #=> true
{ :a => 5 } == { :a => 5 }  #=> true

のように、「同値判定」として自然な結果になるように実装されています。

==演算子に関しては、異なるクラス同士で比較してtrueになり得るパターンはそうないと思いますし*2、===演算子に比べれば気をつけなければいけないケースも少なそうです。

*1:戻り値がtrueもしくはfalseのみ

*2:Numericのサブクラス同士の比較くらいでしょうか?