Ruby2.7の新機能PatternMatchingが最高でした

※ [はてなブログで投稿した記事](https://makicamel.hatenablog.com/entry/2019/05/07/044027)と同じです。

## This is 何？

RubyKaigi2019で聞いたRuby2.7から入る**Pattern Match**の機能に感動したのですが、セッション中は理解しきれない部分があったので、スライドを読み、コードを動かしてみました。
そしたら改めて感動した、という記事です！

[スライド](https://speakerdeck.com/k_tsj/pattern-matching-new-feature-in-ruby-2-dot-7)

スライドのはじめに、下記の記載があります。

- PatternMatchingは2.7.0からの新機能ですが、trunkにはもうcommit済
- 仕様はまだ策定中
- 試してフィードバックくださいね！

なお、[githubにサンプルコード](https://github.com/makicamel/pattern_matching_practice)を置いています。

# 1. 準備

Ruby2.7.0(dev)はビルドしなきゃかな…と思ってたらrbenvがもう対応してました。
はやい！うれしい！

```bash
$ brew upgrade rbenv ruby-build
```

これで無事`2.7.0-dev`がリストに出てくるようになります。

```bash
$ rbenv install --list
  (略)
  2.6.1
  2.6.2
  2.6.3
  2.7.0-dev
  (略)
  
$ rbenv install 2.7.0-dev
(略)
Installed ruby-trunk to /Users/makicamel/.rbenv/versions/2.7.0-dev

$ rbenv versions
* system
  2.7.0-dev

$ rbenv local 2.7.0-dev

$ rbenv version
2.7.0-dev (set by /Users/makicamel/pattern_match/.ruby-version)
```

これで準備完了です！

試しにPatternMatchingを書いてみると動くようになりました！
この時warningが出るのですが、ほんと開発中って感じが、わくわくします…！

```
warning: Pattern matching is experimental,
and the behavior may change in future versions of Ruby!
```

# 2. PatternMatchingとは？

`case`句に対して複数の値を割り当てられること。

従来の`case`句ってこんな感じ。完全一致です。

```ruby
case [0, [1, 2, 3]]
when [0]
  :unreachable
when [0, [1]]
  :unreachable
when [0, [1, 2, 3]]
  p 'here'
end
# => here
```

PatternMatchingが導入されるとこうなります。(`in`節になります)

```ruby
case [0, [1, 2, 3]]
in [0, [1]]
  :unreachable
in [a, b]
  p a # => 0
  p b # => [1, 2, 3]
in [0, [1, 2, 3]]
  :unreachable
end
```

パターンマッチされるだけでなく、マッチした変数名でそのままマッチした値が取り出せます。

`*`(スプラット演算子)を使ってこんな風にもかけます。

```ruby
case [0, [1, 2, 3]]
in [a, [b, *c]]
  p a # => 0
  p b # => 1
  p c # => [2, 3]
end
```

ちゃんと構造もチェックしてくれるので、こうなります。

```ruby
case [0, [1, 2, 3]]
in [a]
  :unreachable
in a
  p a # => [0, [1, 2, 3]]
end
```

ハッシュも使えます。

```ruby
case {a: 0, b: 1}
in {a: 0, x: 1}
  :unreachable
in {a: 0, b: var}
  p var # => 1
end
```

すごい使い所ありそう！
セッションによると、JSONのデータを扱う時に便利ですよ、とのこと。

```ruby
person = '{
  "name": "Alice",
  "children": [
      {
          "name": "Bob",
          "age": 2
      }
  ]
}'

case JSON.parse(person, symbolize_names: true)
in {name: 'Alice', children: [{name: 'Bob', age: age}]}
  p age # => 2
end
```

シンプル！！

# 3. 仕様について

## Syntax

1. 最初にマッチするまで実行される
2. どのパターンもマッチしない場合、`else` 節が実行される
3. どのパターンもマッチせず`else`節もない場合、`NoMatchinPatternError`が発生する。

1と2は現状のcaseと同じですね。
どの条件にも一致しない場合は例外が発生するので、パターンマッチを使う時には網羅性を確認してください、とのことでした。

4.`if/unless`で条件づけができる

```ruby
case [0, 1]
in [a, b] unless a == b
  p a # => 0
  p b # => 1
end
```

a, bが先に評価され、マッチした時に`if/unless`が評価されます。
なので、マッチした値を利用した評価が可能。すごい！

## Pattern

### 1. ValuePattern

`case/when`と同様に、`===`で評価されます。

```ruby
case 0
in 0
in -1..1
in Integer
end
```

どれともマッチします。`when`と同じ挙動。

### 2. VariablePettern

先程までの例のように値をマッチさせ、その変数と値が結び付けられます。
また、`_`を使うと値を捨てて、ワイルドカード的に使えます。

```ruby
case [0, 1]
in [_, _]
  :reachable
end
```

注意点としては、`in`節で変数を使うと、`case`句の外で変数を定義していてもパターンマッチされてしまうこと。
パターンマッチではなく定義された変数として使いたい場合は`^`を使います。

```ruby
a = 0
case 1
in a
  p a # => 1
end

a = 0
case 1
in ^a
  :unreachable
end
# => NoMatchingPatternError
```

### 3. AlternativePattern

case文を書き始めてしばらくして、こんな感じで書けたら…と思った記憶があります。
現実になった！すごい！

```ruby
case 0
in 0 | 1 | 2
  :reachable
end
```

### 4. `As` Pattern

パターンがマッチした時、`=>`で変数名を指定することで変数と値を結び付けられます。
この使い勝手のよさがすごい。
ValuePatternだけじゃなくて、他のパターンでも使えます。

```ruby
case 0
in Integer => a
  p a # => 0
end

case 0
in 0 | 1 | 2 => a
  p a # => 0
end

case [0, [1, 2]]
in [0, [1, _] => a]
  p a # => [1, 2]
end
```

### 5. ArrayPattern

ArrayPatternとはいうけれど、Array以外でも使えます。
以下の3つを満たす時にマッチします。

1. Constant === objectの時
2. objectがArrayを返すdeconstructメソッドを持つ時
3. ネストしたobjectが2の条件を満たす時

また、パターンの書き方は下記のいずれも可能です。

```ruby
Constant(pattern, pattern, ..., *var, pattern)
Constant[pattern, pattern, ..., *var, pattern]
[pattern, pattern, ..., *var, pattern]
```

```ruby
case [0, 1, 2]
in Array(0, *a, 2)
in Object[0, *a, 2]
in [0, *a, 2]
in 0, *a, 2 # `[]`は省略できる
end
p a # => [1]
```

この4つは全て同じ結果。
ふと気になったのが、`in [0, *a, 2]`って最初の例と同じ。
`deconstruct`メソッド実装しなくていいの？と思って確認してみると、`[].methods.include? :deconstruct # => true`でした。
Ruby2.6.2では`[].methods.size`が`188`、2.7.0-devでは`189`になっていて、おお…となりました。

さて、先述した通り、ArrayPatternはArray以外でも使えます。
objectがArrayを返せばOKなので、実装してあげます。

```ruby
class Struct
  alias deconstruct to_a
end

Color = Struct.new(:r, :g, :b)
color = Color[0, 10, 20]
p color.deconstruct # => [0, 10, 20]
case color
in Color[0, 0, 0]
  p 'black'
in Color[255, 0, 0]
  p 'red'
in Color[r, g, b]
  p "#{r}, #{g}, #{b}"
end
```

StructがArrayを返すよう、`deconstruct`を`to_a`のエイリアスに設定しています。

スライドにはASTの例もありましたが、理解が追いつかないのでレベルアップしてから再挑戦します。

### 6. HashPattern

HashPatternも、Hash以外でも使えます。
以下の3つを満たす時にマッチします。

1. Constant === objectの時
2. objectがHashを返すdeconstruct_keysメソッドを持つ時
3. ネストしたobjectが2の条件を満たす時

また、パターンの書き方もArray同様下記のいずれも可能です。

```ruby
Constant(id: pattern, id: pattern, ..., **var)
Constant[id: pattern, id: pattern, ..., **var]
{id: pattern, id: pattern, ..., **var}
```

Arrayに引き続き確認すると、`{}.methods.include? :deconstruct_keys # => true`。
ただし今回はRuby2.6.2では`{}.methods.size`が`174`、2.7.0-devでは`176`。
ひとつは`deconstruct_keys`として、もうひとつは？と確認してみると、`:tally`でした。

`tally`は[配列の要素数を要素毎に数え上げるEnumerableモジュールのメソッド](https://medium.com/@baweaver/ruby-2-7-enumerable-tally-a706a5fb11ea)。
こちらも楽しみです😊

さて、コードを見てみます。

```ruby
case {a: 0, b: 1}
in Hash(a: a, b: 1)
  p a # => 0
in Object[a: a]
in {a: a}
in {a: a, **rest}
  p rest # => {b: 1}
end
```

`{}`は省略可。変数名も省略できます(`a:` == `a: a`)。

```ruby
case {a: 0, b: 1}
in a:, b:
  p a # => 0
  p b # => 1
end
```

また、`deconstruct_keys`の実装で思いがけない値を返すと逆効果になるので、実装に注意してくださいとのことでした。

- `deconstruct_keys`に渡される`keys`はパターンに含まれる`key`の配列
- `keys`に含まれていない`key`は無視してOK
- `**rest`がパターンに含まれる場合はnilが渡る
- その場合、全てのkey-valueセットを返さなければならない

コードを見たほうがわかりやすそうです。

```ruby
class Time
  VALID_KEYS = %i(year month)

  def deconstruct_keys(keys)
    if keys
      (VALID_KEYS & keys).each_with_object({}) do |k, h|
        h[k] = send(k)
      end
    else
      {year: year, month: month}
    end
  end
end

now = Time.now # 2019-05-07 ...
case now
in year: # now.deconstruct_keys([:year])を呼ぶ
  p year # => {year: 2019}
in **rest # now.deconstruct_keys(nil)を呼ぶ
  p rest  # => {year: 2019, month: 5}
end
```

ArrayとHashの違いで気をつけたいのは、**Arrayは完全一致、Hashはサブセットマッチなこと**。
ArrayとHashでは使い所が違うから、とのことでした。

```ruby
case [0, 1]
in [a]
  :unreachable
in [a, *]
  :reachable
end

case {a: 0, b: 1}
in {a: a}
  :reachable
end
```

## デザイン

Historyは時間がなくてセッション中端折られてしまったのですが、スライドによると2012年にgemを作ったことから端を発して、7年かけて作られた機能。
最初は`match/pattern`だったり`case/=>`になっていったり、時間をかけて様々考えられ、磨きぬかれていった様にわくわくします！

キーワードも、`match`でなく`case`したのは、`match`だと既存のコードを壊してしまう可能性があるから。
新しい予約語を使わないために最初は記号を使ったりしようとしていたのだけど、そうだ、僕らには`for/in`の`in`があるじゃないか！と思いついた話がすごく好きです。

なるべく自然に書けるように、でも既存のコードを壊さないように。
コードを試し書きしていて何度も気持ちいいなあ、と思ったのですが、Rubyの「開発者が楽しい」ってこうして守られ、作られているんだなあ、と改めて感じ入りました。

また、まだもっとよくしたいんだけど、どう思う？というスライドもたくさんあって、わくわくしました。

パターンマッチング、楽しみです！