Ruby 2.7 の変更点 - パターンマッチング

Ruby 2.7 アドベントカレンダーの2日目の記事です。(更新が遅いのは仕様です)

qiita.com

パターンマッチング

パターンマッチングは 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"

さらにパターンには ifunless で条件を追加することもできます。

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 の発表資料を見ましょう。

rubykaigi.org

[追記] どうやら 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"

やっぱり慣れなのかな。一年くらい経ったら普通に便利に使ってるような気もしなくもない。