Ruby2.7の新機能PatternMatchingが最高でした
RubyKaigi2019で聞いたRuby2.7から入るPattern Matchの機能に感動したのですが、セッション中は理解しきれない部分があったので、スライドを読み、コードを動かしてみました。
そしたら改めて感動した、という記事です!
スライドのはじめに、下記の記載があります。
- PatternMatchingは2.7.0からの新機能ですが、trunkにはもうcommit済
- 仕様はまだ策定中
- 試してフィードバックくださいね!
なお、githubにサンプルコードを置いています。
1. 準備
Ruby2.7.0(dev)はビルドしなきゃかな…と思ってたらrbenvがもう対応してました。
はやい!うれしい!
$ brew upgrade rbenv ruby-build
これで無事2.7.0-dev
がリストに出てくるようになります。
$ 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
句ってこんな感じ。完全一致です。
case [0, [1, 2, 3]] when [0] :unreachable when [0, [1]] :unreachable when [0, [1, 2, 3]] p 'here' end # => here
PatternMatchingが導入されるとこうなります。(in
節になります)
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
パターンマッチされるだけでなく、マッチした変数名でそのままマッチした値が取り出せます。
*
(スプラット演算子)を使ってこんな風にもかけます。
case [0, [1, 2, 3]] in [a, [b, *c]] p a # => 0 p b # => 1 p c # => [2, 3] end
ちゃんと構造もチェックしてくれるので、こうなります。
case [0, [1, 2, 3]] in [a] :unreachable in a p a # => [0, [1, 2, 3]] end
ハッシュも使えます。
case {a: 0, b: 1} in {a: 0, x: 1} :unreachable in {a: 0, b: var} p var # => 1 end
すごい使い所ありそう!
セッションによると、JSONのデータを扱う時に便利ですよ、とのこと。
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
- 最初にマッチするまで実行される
- どのパターンもマッチしない場合、
else
節が実行される - どのパターンもマッチせず
else
節もない場合、NoMatchinPatternError
が発生する。
1と2は現状のcaseと同じですね。
どの条件にも一致しない場合は例外が発生するので、パターンマッチを使う時には網羅性を確認してください、とのことでした。
4.if/unless
で条件づけができる
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
と同様に、===
で評価されます。
case 0 in 0 in -1..1 in Integer end
どれともマッチします。when
と同じ挙動。
2. VariablePettern
先程までの例のように値をマッチさせ、その変数と値が結び付けられます。
また、_
を使うと値を捨てて、ワイルドカード的に使えます。
case [0, 1] in [_, _] :reachable end
注意点としては、in
節で変数を使うと、case
句の外で変数を定義していてもパターンマッチされてしまうこと。
パターンマッチではなく定義された変数として使いたい場合は^
を使います。
a = 0 case 1 in a p a # => 1 end a = 0 case 1 in ^a :unreachable end # => NoMatchingPatternError
3. AlternativePattern
case文を書き始めてしばらくして、こんな感じで書けたら…と思った記憶があります。
現実になった!すごい!
case 0 in 0 | 1 | 2 :reachable end
4. As
Pattern
パターンがマッチした時、=>
で変数名を指定することで変数と値を結び付けられます。
この使い勝手のよさがすごい。
ValuePatternだけじゃなくて、他のパターンでも使えます。
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つを満たす時にマッチします。
- Constant === objectの時
- objectがArrayを返すdeconstructメソッドを持つ時
- ネストしたobjectが2の条件を満たす時
また、パターンの書き方は下記のいずれも可能です。
Constant(pattern, pattern, ..., *var, pattern)
Constant[pattern, pattern, ..., *var, pattern]
[pattern, pattern, ..., *var, pattern]
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なので、実装してあげます。
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つを満たす時にマッチします。
- Constant === objectの時
- objectがHashを返すdeconstruct_keysメソッドを持つ時
- ネストしたobjectが2の条件を満たす時
また、パターンの書き方もArray同様下記のいずれも可能です。
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モジュールのメソッド。
こちらも楽しみです😊
さて、コードを見てみます。
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
)。
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セットを返さなければならない
コードを見たほうがわかりやすそうです。
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では使い所が違うから、とのことでした。
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の「開発者が楽しい」ってこうして守られ、作られているんだなあ、と改めて感じ入りました。
また、まだもっとよくしたいんだけど、どう思う?というスライドもたくさんあって、わくわくしました。
パターンマッチング、楽しみです!