Ruby の trunk に（実験的に）パターンマッチ構文が入った！

[ブログ記事](http://secret-garden.hatenablog.com/entry/2019/04/17/175435)からの転載です。

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

* [Introduce pattern matching [EXPERIMENTAL] · ruby/ruby@9738f96 · GitHub](https://github.com/ruby/ruby/commit/9738f96fcfe50b2a605e350bdd40bd7a85665f54)
* [Feature #14912: Introduce pattern matching syntax - Ruby trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/14912)

NOTE: 実験的に導入されたので今後仕様が変わる可能性があるので注意してください。


## 試してみた

と、言うことで早速試してみました。
`case when` と構文が似ていますが『抽象的にマッチしつつ、マッチした値を変数にキャプチャすることが出来る』っていうあたりが違います。
簡単な使用例としてはこんな感じになります。

```ruby
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` 文で書くとこんな感じですかね。

```ruby
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` を次のように書くことが出来ます。

```ruby
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` を記述することができ、次のような判定を行うことも出来ます。

```ruby
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)` のような記法を記述することも出来ます。
これは例えば次のように利用できます。

```ruby
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` を呼び出す事が出来るようになっているので次のように記述することも出来ます。

```ruby
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 で最終的にどうなるのかが楽しみです。
個人的には条件を付加しつつ、変数にキャプチャ出来るようになると嬉しいですねえ。