Ruby の trunk に(実験的に)パターンマッチ構文が入った!

Ruby の trunk にパータンマッチ構文が実験的に導入されました。

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 メソッドを呼び出すことが出来ます。

inHash を渡す

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 の構文を見たときは戸惑ったんですが、実際に使ってみると結構理にかなった構文になっている事がわかってきました。
変数に値をキャプチャすることが出来るのがかなりパターンマッチっぽくてよい。
あと ArrayHash などが使えるのはもちろんなんですが、 #deconstruct を定義することでいろんなクラスでも応用出来るのでかなり便利そうですね。
今後どうなるのかわからない Ruby 2.7 や Ruby 3.0 で最終的にどうなるのかが楽しみです。
個人的には条件を付加しつつ、変数にキャプチャ出来るようになると嬉しいですねえ。