Ruby の trunk に(実験的に)パターンマッチ構文が入った!
Ruby の trunk にパータンマッチ構文が実験的に導入されました。
- Introduce pattern matching [EXPERIMENTAL] · ruby/ruby@9738f96 · GitHub
- Feature #14912: Introduce pattern matching syntax - Ruby trunk - Ruby Issue Tracking System
NOTE: 実験的に導入されたので今後仕様が変わる可能性があるので注意してください。
試してみた
と、言うことで早速試してみました。
case when
と構文が似ていますが『抽象的にマッチしつつ、マッチした値を変数にキャプチャすることが出来る』っていうあたりが違います。
簡単な使用例としてはこんな感じになります。
def func *x case x in [a] -a in [a, b] a + b in [a, b, c] a * b * c end end p func(1) # => -1 p func(2, 3) # => 5 p func(4, 5, 6) # => 120
case when
と似ていますがパターンマッチでは when
ではなくて in
というキーワードを使用してマッチする構文を記述します。
上記の場合は Array
にマッチしつつ、配列の要素を各変数でキャプチャしています。
無理やり if
文で書くとこんな感じですかね。
def func *x if Array === x && x.size == 1 a, = *x -a elsif Array === x && x.size == 2 a, b = *x a + b elsif Array === x && x.size == 3 a, b, c = *x a * b * c end end
パターンマッチを利用した FizzBuzz
これを利用すると FizzBuzz
を次のように書くことが出来ます。
def fizzbuzz a case [a % 5 == 0, a % 3 == 0] in [true, true] "FizzBuzz" in [_, true] "Fizz" in [true, _] "Buzz" else a end end # self.:fizzbuzz は method(:fizzbuzz) と等価 p (1..15).map &self.:fizzbuzz # => [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
上記の場合はキャプチャした変数は使っていませんが『何でも受け取れる要素』として変数のキャプチャを利用しています。
だいぶすっきりと書くことが出来ますね。
余談ですが Ruby 2.7 では .:
演算子で method
メソッドを呼び出すことが出来ます。
in
に Hash
を渡す
in
には Hash
を記述することができ、次のような判定を行うことも出来ます。
def validation user case user in { name: /^[a-z]+$/, age: Integer } :ok else :ng end end p validation(name: "homu", age: 14) # => :ok p validation(name: "42", age: 14) # => :ng p validation(name: "mami", age: "14") # => :ng
これはかなり便利そう。
in User(name, age)
記法
in
には in User(name, age)
のような記法を記述することも出来ます。
これは例えば次のように利用できます。
class User attr_accessor :name, :age def initialize name, age self.name = name self.age = age end def deconstruct [name, age] end end def show user case user in User(name, age) p "User(name = #{name}, age=#{age})" in [name, age] p "name=#{name}, age=#{age}" end end homu = User.new("homu", 14) show(homu) # => "User(name = homu, age=14)" show(["mami", 15]) # => "name=mami, age=15"
in User(name, age)
と記述された場合、まず user.kind_of?(User)
で判定が行われ、マッチしていれば user.deconstruct
メソッドで値のキャプチャが行われます。
#deconstruct
メソッドは case when
でいう #===
のようなメソッドで、(多分)パターンマッチ時に内部で暗黙的に呼ばれるメソッドになります。
これを定義することで独自クラスに対してもパターンマッチを行うことが出来ます。
例えば Struct
で #deconstruct
を呼び出す事が出来るようになっているので次のように記述することも出来ます。
User = Struct.new(:name, :age) def show user case user in User(name, age) p "User(name = #{name}, age=#{age})" in [name, age] p "name=#{name}, age=#{age}" end end homu = User.new("homu", 14) show(homu) # => "User(name = homu, age=14)" show(["mami", 15]) # => "name=mami, age=15"
これなかなかに便利そう
所感
最初に case in
の構文を見たときは戸惑ったんですが、実際に使ってみると結構理にかなった構文になっている事がわかってきました。
変数に値をキャプチャすることが出来るのがかなりパターンマッチっぽくてよい。
あと Array
や Hash
などが使えるのはもちろんなんですが、 #deconstruct
を定義することでいろんなクラスでも応用出来るのでかなり便利そうですね。
今後どうなるのかわからない Ruby 2.7 や Ruby 3.0 で最終的にどうなるのかが楽しみです。
個人的には条件を付加しつつ、変数にキャプチャ出来るようになると嬉しいですねえ。