読者です 読者をやめる 読者になる 読者になる

定食屋おろポン

おろしポン酢と青ネギはかけ放題です

RubyのModule#===について

=====のおさらいは飛ばして記事の下の方を読む

==とか===とかequalとか

==での同値性判定は暗黙の型変換を行ってゆるーくふわっと判定するけど、===は型変換を行わずに同値性判定を行う言語ってありますよね。
PHPとかJavaScriptとか。

一方で、==はオブジェクトの同一性判定を行うため、同値性判定を行うにはa equals(b)とか[a isEqualTo*:b]とか書く言語もありますよね。
JavaとかObjective-Cとか。

Rubyのお話

Ruby==について

Rubyでは、==はメソッドなので、同値性判定なのか同一性判定なのかもよくわからない可愛いやつです。

==は同一性判定を行うように定義されていますが、同値性判定を行うように==をオーバーライドすることを期待されています。*1

逆に、equal?はオーバーライドしてはいけません。

Unlike ==, the equal? method should never be overridden by subclasses as it is used to determine object identity Class: BasicObject (Ruby 2.1.0)

例えばStringは==で同値性を判定できます。

a = "hoge"
b = "hoge"
puts a.object_id #=> 70312919003820
puts b.object_id #=> 70312919003800
puts a.equal? b #=> false
puts a == b #=> true

ただ、自前で実装したクラスに==を定義していないと同値性判定ではなく同一性判定を行うので、ちゃんとオーバーライドしてないのに同値性判定を期待すると痛い目にあいます。

class Cat
  attr_reader :name, :age
  def initialize name, age
    @name = name
    @age = age
  end
end

mike = Cat.new("mike",5)
kuro = Cat.new("kuro",3)

puts mike != kuro #=> true
puts [mike, kuro].include? Cat.new("mike",5) #=> false

Ruby===について

PHPJavaScriptとは違い、Ruby===は「==より厳密な同値性判定を行う」といった性質のものではありません。

===はcase式のためにあるような演算子メソッドで、

case a
when b then hogehoge()
when c then fugafuga()
end

case式の中でaとbを比較するために===が使用されます。

ここで注意すべきなのは、a === bではなく、b === aが呼ばれるということです。 これらは全く違います。 ===は単なる演算子ではなくメソッドなので、どちらがレシーバで、どちらが引数なのかはとても重要です。

メソッド呼び出しであることがわかりやすいように書き直すと、a.===(b)ではなくb.===(a)が呼ばれます。

===のオーバーライド時に一般に期待されるのは、「所属性」を返すことです。*2

b.===(a)に求められるのは、「引数aは、bに所属しているか」を返すことです。

よくRubyのcase式の例で挙げられる、Range#===は、Range#include?Range#member?と全く同じです。*3

そのため、こんなクールなcase式を書けます。

case 1
when (1..3) then puts "Expected"
else puts "Oops.."
end

#=> Expected

しかし、Rangeオブジェクトそのものをcase式で比較すると、意に反した動作をします。 (1..3) === (1..3)、つまり(1..3).include? (1..3)falseだからです。

range = (1..3)
case range
when (1..3) then puts "Expected"
else puts "Oops.."
end

#=> Oops..

Module#===について

たとえば、引数に名前か年齢を受け取るメソッドを考えてみます。 名前を渡されたら挨拶を、年齢を渡されたら年齢を表示するメソッドです。

def a_method name_or_age
  # 名前なら、"hello, name!"と出力
  # 年齢なら、"You are age years old"と出力
end

そこで、引数の型チェックを行ってみようと思います。

ぱっと浮かぶのはこんなコードでしょうか。

def a_method name_or_age
  case name_or_age.class
  when String then puts "hello, #{name_or_age}"
  when Fixnum then puts "You are #{name_or_age} years old"
  else raise ArgumentError
  end
end

name_or_ageのクラスによって分岐して、Stringなら挨拶を、Integerなら年齢を表示します。

これを実行してみます。

a_method "oropon" #=> in `a_method': ArgumentError (ArgumentError)
a_method 27 #=> in `a_method': ArgumentError (ArgumentError)

動きません!StringでもFixnumでもないよ、と怒られてしまいます。

でも、==で比較してみるとちゃんとtrueが返ってきます。

puts "oropon".class == String #=> true
puts 27.class == Fixnum #=> true

==での比較はtrueなのに、===で比較するとfalseなの? =====より厳格じゃないの??

YES!! 上でRangeの例を挙げたように、Rubyでは=====は厳格さが違うのではなく、用途が違うんです!

先ほどのコードを書き直すと、こんな感じになります。

def a_method name_or_age
  case name_or_age
  when String then puts "hello, #{name_or_age}"
  when Fixnum then puts "You are #{name_or_age} years old"
  else raise ArgumentError
  end
end

a_method "oropon" #=> hello, oropon
a_method 27 #=> You are 27 years old

caseの右にあるname_or_age.classから、.classが取れてname_or_ageだけになりました。

これだけで何故うまく動くのかの答えは、Module#===の実装にあります。 StringやFixnumはClassクラスのインスタンスで、ClassクラスはModuleクラスのサブクラスです。Class#===は、継承元のModule#===が呼び出されます。

ドキュメントを見てみます。

self === obj -> bool

指定された obj が自身かそのサブクラスのインスタンスであるとき真を返します。 また、obj が自身をインクルードしたクラスかそのサブクラスのイン スタンスである場合にも 真を返します。上記のいずれでもない場合に false を返します。

言い替えると obj.kind_of?(self)true の場合、 true を返します。

このメソッドは主に case 文での比較に用いられます。 case ではクラス、モジュールの所属関係をチェックすることになります。

instance method Module#===

これで謎が解けました。

String === "oropon".classは、"oropon".class.kind_of?(String)ということで、これは「StringクラスはStringクラス*4インスタンスですか?」と聞いているに等しいわけです。もちろん返り値はfalseです。

ついでに、kind_of?で比較するのでこう書き換えられます。

def a_method name_or_age
  case name_or_age
  when String then puts "hello, #{name_or_age}"
  when Integer then puts "You are #{name_or_age} years old" # Fixnum -> Integer
  else raise ArgumentError
  end
end

a_method "oropon" #=> hello, oropon
a_method 27 #=> You are 27 years old

FixnumIntegerに書き換えたことで、引数がFixnumでもBignumでも年齢として扱います。

以上、「case式は、==が通っても===は通らないことがあるから気をつけろよ!」「case式や===の挙動は直感だけに頼らずちゃんと確認しろよ!」というお話でした。