Ruby 2.7 アドベントカレンダーの2日目の記事です。(更新が遅いのは仕様です)
パターンマッチング
パターンマッチングは Ruby 2.7 での目玉機能と言ってもいいでしょう。 ただし現時点では experimental で、使用すると次のメッセージが出力されます。
warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
パターンマッチングは配列やHashなどの構造のパターンとオブジェクトをマッチングするためのものです。
パターンマッチングは case
...in
構文を使用します。
case
に指定したオブジェクトのパターンが、in
で指定したパターンと一致していれば、パターンに指定した変数に対応する値が代入されて、in
部が実行されます。
obj = [0, [1, 2, 3]] case obj in [a, [b, *c]] p a #=> 0 p b #=> 1 p c #=> [2, 3] end
配列の場合は要素数が一致していないとマッチしませんが、Hash の場合はパターンに書かれたキーが適合すれば、オブジェクトに他のキーが存在していても構いません。
obj = {a: 0, b: 1, c: 2} case obj in {a: 0, x: 1} # ここは通らない in {a: 0, b: var} p var #=> 1 end
JSONをパースした結果の構造から値を取り出す時に便利ですね。
require 'json' json = '{"a":123, "b":[0, {"c":456, "d":{"e": 789}}]}' case JSON.parse(json, symbolize_names: true) in {b: [_, {c: _, d: {e: var}}]} p var #=> 789 end
適合するパターンが無ければエラーになります。
obj = [1, "hoge"] case obj in {a: var} # 適合しない in [a, b, c] # 適合しない end #=> [1, "hoge"] (NoMatchingPatternError)
else
を書いておくとエラーにならずに else
部が実行されます。
obj = [1, "hoge"] case obj in {a: var} # 適合しない in [a, b, c] # 適合しない else "no match" end #=> "no match"
さらにパターンには if
や unless
で条件を追加することもできます。
def hoge(obj) case obj in [a, b] if a == b.to_i true else false end end hoge [123, "123"] #=> true hoge [123, "456"] #=> false hoge "xxxx" #=> false
パターンに変数を使用したい場合は ^
をつけて記述します。
def hoge(obj) pattern = [1, 2] case obj in ^pattern true else false end end hoge [1, 2] #=> true hoge [3, 4] #=> false
型を指定することもできます。その場合は =>
で割り当てる変数を指定します。
def hoge(obj) case obj in [String => s, Integer => n] s * n in [a, b] a + b end end hoge ["abc", 2] #=> "abcabc" hoge [3, 4] #=> 7
その他、詳しい情報は RubyKaigi 2019 の発表資料を見ましょう。
[追記] どうやら case
の中ではなく in
だけでも使えるようです。
obj = [123, "abc"] obj in [a, b] p a #=> 123 p b #=> "abc"
obj in pattern
はマッチしたかどうかを真偽値で返すので、if
で使えて便利。
obj = [123, "abc"] if obj in [a, b] p a #=> 123 p b #=> "abc" end
[追記の追記] これは 2.7.0-rc1 でできなくなりました。真偽値を返さなくなり、マッチしなかった場合に例外があがります。
というわけで便利そうなんですけど、個人的にはなんかモニョってたり。
in
の後ろはパターンだと思えばいいのかもしれないけど、Ruby には for
...in
というのがあって、この場合は従来どおりの配列なんですな。
for v in [1, 2, 3] p v end
まあでも for
は誰も使ってないからいいや(暴言)。
case
...when
と形式が似てるのがアレなのかな…。
case obj when Array # obj が Array であれば実行 end case obj in Array # obj が Array であれば実行 end
あれ? 同じだな…。じゃあ case
の一種でいいのか。
やっぱりパターン内に変数があるのがしっくりこないのかな。
[var]
という表記は、今までの Ruby の構文だと var
オブジェクトを要素として持つ配列を表していたので、当然 var
はそれよりも前に宣言された変数でないとエラーになってたんだけど、in [var]
はパターンなので、var
は変数参照ではなくパターンにマッチした時に値が代入される変数として扱われる…というのが。
突然未定義変数っぽい字面のものが表れてびっくりするというか…。
代入っぽくないのに変数に値が代入されるものとしては、既に正規表現の名前付きキャプチャなんてのもあるけど…。
/(?<hoge>[a-z]*)(?<fuga>[0-9]*)/ =~ 'abc123' hoge #=> "abc" fuga #=> "123"
やっぱり慣れなのかな。一年くらい経ったら普通に便利に使ってるような気もしなくもない。