【一人 Ruby Advent Calendar 2017】case-when と === 演算子について【3日目】
一人 Ruby Advent Calendar 2017 3日目の記事になります。
さて、今回は case
式の when
節と #===
演算子について簡単に解説してみたいと思います。
case
式
さてさて、Ruby の case
式は以下のようにして when
節と組み合わせて使います。
def func n case n # n の値が when に渡した値であればその式を評価して返す when 1 "one" when 2 "two" when 3 "three" else "other" end end p (1..5).map &method(:func) # => ["one", "two", "three", "other", "other"]
簡単な例だとこんな感じですね。
また、when
節には複数の引数を渡すことができます。
def func n case n # 引数のいずれかとマッチすれば OK when 1, 2, 3 "low" when 4, 5, 6 "middle" when 7, 8, 9 "high" end end p (1..10).map &method(:func) # => ["low", "low", "low", "middle", "middle", "middle", "high", "high", "high", "other"]
ちなみに Ruby の case
は式なので該当する when
節を評価した値を返します。
when
節はどのようにして判定しているのか
さて、では case-when
がどのように処理されるのか見てみましょう。
例えば、以下のような case-when
があった場合、
case n when 1, 2 "one, two" when 3 "three" else "other" end
これは以下のようなコードとほぼ等価です。
_tmp = n if 1 === _tmp || 2 === _tmp "one, two" elsif 3 === _tmp "three" else "other" end
こんな感じで複数の if
式で条件分岐しているのと同じですね。
注目すべきは 1 === _tmp
のように比較演算子に『#===
演算子を使っている点』です。
Ruby における #===
演算子とは
では、Ruby における #===
演算子とはどういう役割なんでしょうか。
Rubyリファレンスマニュアルでは Object#===
について以下のように記述されています。
メソッド Object#== の別名です。 case 式で使用されます。このメソッドは case 式での振る舞いを考慮して、 各クラスの性質に合わせて再定義すべきです。
一般的に所属性のチェックを実現するため適宜再定義されます。
when 節の式をレシーバーとして === を呼び出すことに注意してください。
また Enumerable#grep でも使用されます。
https://docs.ruby-lang.org/ja/latest/class/Object.html#I_--3D--3D--3D
このように #===
はcase
や Enumerable#grep
のような『特定の構文やメソッド』で使用されることを想定しています。
そのため case
や Enumerable#grep
等で振る舞いを変更したい場合はそのクラスの性質に合わせて #===
再定義します(されています)。
Ruby 以外の言語、例えば JavaScript では #===
演算子は『厳密等価演算子』と呼ばれその名の通り『==
よりも厳密に値の比較を行う』という目的で使用されます。
しかし、このように Ruby では全く違う目的で使用されるので混同しないように注意しておく必要があります。
ちなみにデフォルトの #===
演算子は ==
演算子と同等になります。
#===
が定義されているクラスいろいろ
Ruby では主に以下のようなクラスで #===
演算子が定義されています。
オブジェクト | obj === other の挙動 |
---|---|
Range | other が範囲内に含まれているかどうか。 obj.include? other と等価 |
Regexp | 文字列 other で正規表現マッチを行う |
Class | other がそのクラス(obj )のインスタンスであるかどうか |
Proc | other を引数とした obj.call を呼び出す |
なので上記のようなオブジェクトであれば when
節でいい感じに使うことができます。
Range
の場合
p (1..10) === 3 # => true p (1..10) === 25 # => false p (1..10) === -5 # => false def level n case n # 1〜3の範囲にマッチする場合みたいに記述することが出来る when (1..3) "low" when (4..6) "middle" when (7..9) "high" else "other" end end p (1..10).map &method(:level) # => ["low", "low", "low", "middle", "middle", "middle", "high", "high", "high", "other"]
Regexp
の場合
p /^\d+/ === "234" # => true p /^\d+/ === "homu" # => false def check str case str # 数値の場合 when /^\d+$/ "#{str} is number" # 英字の場合 when /^\a+$/ "#{str} is string" else "#{str} is other" end end p check "1234" # => "1234 is number" p check "homu" # => "homu is other" p check "homu1234" # => "homu1234 is other"
Class
の場合
p String === "homu" # => true p Numeric === "homu" # => false p Numeric === 1234 # => true p Numeric === 3.14 # => true def twice x case x # x が各クラスのインスタンスだった場合 when Numeric x + x when String x.to_i + x.to_i when Symbol x.to_s.to_i + x.to_s.to_i else "???" end end p twice 42 # => 84 p twice "1234" # => 2468 p twice :"5678" # => 11356 p twice [1, 2, 3] # => "???"
Proc
の場合
p proc { |a| a + a } === 5 # => 10 def level n case n # Proc によりより詳細な条件を記述することが出来る when proc { |n| n < 4 } "low" when proc { |n| n < 7 } "middle" when proc { |n| n < 10 } "high" else "other" end end p (1..10).map &method(:level) # => ["low", "low", "low", "middle", "middle", "middle", "high", "high", "high", "other"]
このように #===
演算子が再定義されているクラスは case-when
でただ比較するだけではないような使い方ができます。
応用
さて、case-when
では #===
演算子を再定義することで柔軟に挙動を変更することが出来ることがわかったと思います。
これを応用して例えば Array#===
を再定義すると次のようにコードを書くことが出来るようになります。
# Array#=== を定義 using Module.new { refine Array do def === other, &block size == other.size && zip(other).all? { |a, b| a.=== b, &block } end end } # 必ず true を返す proc を返す def _ proc { true } end def fizzbuzz n case [n % 3, n % 5] # n % 3 === 0 && n % 5 === 0 when [0, 0] "FizzBuzz" # n % 3 === 0 when [0, _] "Fizz" # n % 5 === 0 when [_, 0] "Buzz" else n end end p (1..20).map &method(:fizzbuzz)
こんな感じでパターンマッチっぽく FizzBuzz を実装することができます。
まとめ
case-when
ではオブジェクトを比較する時に#===
演算子を使用する- Ruby で
#===
演算子はcase-when
などで使用されることを前提としている #===
演算子を再定義することでcase-when
などで使い勝手がよくなる
と、いうことで簡単にまとめてみました。
思ったよりも Ruby の case-when
は柔軟性が高くて応用が利きそうな感じがしますね。
Ruby の case-when
を C言語のような switch-case
として考えるとあまり使う気かはないかもしれませんが、#===
演算子を意識すると、もしかしたら case-when
を使ったほうが簡単にコードを書くことが出来るかもしれませんね。
また、case-when
以外でも通常の条件分岐として #===
演算子を使うことはできるので、覚えておくと意外なところで利用できるかもしれません。