こんにちは! id:chionyan です。
4/18〜4/20に福岡で開催された RubyKaigi 2019 に参加してきました!
ちょうど1年くらい前に福岡からフィードフォースに来たので、とても感慨深い気持ちでした😋
社内で感想を話したところ、Ruby 2.7.0 で導入予定の Pattern matching について興味津々な方が多かったので、試してみてわかったことをまとめてみます!
Ruby 2.7.0-dev のインストール
rbenv
でインストールできる 2.7.0-dev
では既にパターンマッチングが試せるようになっています。
$ rbenv install 2.7.0-dev
ちなみにパターンマッチングを実行すると、
warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
「パターンマッチングは実験的なもので、将来のバージョンのRubyでは動作が変わる可能性があります。」というメッセージが表示されるので、12月リリース予定の 2.7.0
ではここで書いた内容から変更されているかもしれません。
パターンマッチング簡単ご紹介
パターンマッチングは case/in
で表現されます。
従来の条件分岐として使われている case/when に加えて、多重代入できるようになっています。
case [0, [1, 2, 3]] in [a, [b, *c]] p a #=> 0 p b #=> 1 p c #=> [2, 3] end
また、in
の後にマッチさせたいパターンを書くことで、配列やハッシュのオブジェクト構造のチェックも可能です!
case [0, [1, 2, 3]] in [a] :unreachable in [0, [a, 2, b]] p a #=> 1 p b #=> 3 end
case {a: 0, b: 1} in {a: 0, b: var} p var #=> 1 end
JSON データを扱う時にパターンマッチングの良さがとても見えました!
例えば name: Alice
の children
(一人息子) である name: Bob
の age
を取得する場合、
{ "name": "Alice", "age": 30, "children": [ { "name": "Bob", "age": 2 } ] }
パターンマッチングを使わない条件分岐だと、
person = JSON.parse(json, symbolize_names: true) if person[:name] == "Alice" children = person[:children] if children.length == 1 && children[0][:name] == "Bob" p children[0][:age] #=> 2 end end
対して、パターンマッチングを使った条件分岐だと、
case JSON.parse(json, symbolize_names: true) in {name: "Alice", children:[{name: "Bob", age: age}]} p age #=> 2 end
と書けるようになります!とても読みやすいですね✨
条件分岐って複雑になってくるとネストが深くなってきますが、パターンマッチングを使うととてもシンプルに書けるようになります👏
ちなみに in
を採用した理由は、「ご存知ない方がおおいかもしれないですが、Rubyにはforという文法があります…」という一文に隠されている、という噂ですね…👀
パターンマッチングの処理の流れ
ここからはパターンマッチングの具体的な内容に入っていきます!
基本的な文法は以下の通りです。
case expr in pattern1 [if/unless condition] # 処理1 in pattern2 [if/unless condition] # 処理2 else # 処理3 end
上から順にパターンがマッチするまでパターンマッチングが行われます。
全てのパターンにマッチしない場合は、 else 節の処理が実行されます。
else 節もない場合は、NoMatchingPatternError
例外が発生します。
- 例:
in [a]
にはマッチしないのでスキップ、in [0, [a, 2, b]]
の処理が実行される
case [0, [1, 2, 3]] in [a] :unreachable in [0, [a, 2, b]] p a end #=> 1
- 例:
in [a]
にはマッチしないので、 else 節の処理が実行される
case [0, [1, 2, 3]] in [a] :unreachable else p 'No Matching' end #=> "No Matching"
- 例:
in [a]
にはマッチせず、 else 節もないので、NoMatchingPatternError
例外が発生する
case [0, [1, 2, 3]] in [a] :unreachable end #=> NoMatchingPatternError
だから条件分岐は網羅的に書こうねってお話しもありました!
また、ガード式に対してガード式を使うことができます!
パターンがマッチした場合にのみ、ガード式が評価されて、その結果によって処理が実行されるかが決まります。
else 節もない場合は、NoMatchingPatternError
例外が発生します。
- 例: パターンがマッチして、ガード式が真の場合、処理が実行される
case [0, 1] in [a, b] if a != b p a end #=> 0
- 例: パターンがマッチして、ガード式が偽の場合、
NoMatchingPatternError
例外が発生する
case [0, 1] in [a, b] if a == b p a end #=> NoMatchingPatternError
- 例:パターンがマッチしない場合は、ガード式も評価されず、次のパターンに進む
case [0, 1] in [a, b, c] if a != b p a in [a, b] p b end #=> 1
基本的な構文は case/when
と似ていますが、ガード式などはパターンマッチングで初めて加えられたため、使い方の幅が広がりそうですね😊
パターンのバリエーション
in
の後に書けるパターンは6種類の紹介がありました!
また、パターン記法における登場人物は、
- literal: リテラル
- Constant: 定数
- var: 変数
- pat: パターン
が登場します!
パターンの中にパターンが登場することもあるので、すごい使い方もできそう…!?
Value パターン
パターン記法: literal or Constant
pattern === object
のオブジェクトとマッチします。
[0, 1, 2].each do |var| case var in 0 # 値はもちろん p '0だよ' in -1..1 # Range もオッケー p '-1から1の間だよ' in Integer # 定数もオッケー p '数値だよ' end end #=> "0だよ" #=> "-1から1の間だよ" #=> "数値だよ"
Variable パターン
パターン記法: var
任意の値とマッチして、変数にその値を代入します。
case 0 in a p a end #=> 0
変数に代入する必要がない場合、アンダースコアで省略可能。
case [0, 1] in [_, _] :reachable end
同じ名前の外部変数がある場合、変数の値は上書きされます。
a = 0 case 1 in a p a #=> 1 end p a #=> 1
既存の変数の値に対してパターンマッチングを行う場合は、^
を使用することができます。
a = 0 [0, 1].each do |num| case num in ^a # `case の外側での a の値は 0 なので、`in 0` と同様 p "#{a}だよ" else p "#{a}じゃないよ" end end #=> "0だよ" #=> "0じゃないよ"
Alternative パターン
パターン記法: pat | pat ...
複数のパターンを OR 条件でつなぐことができます。
['abc', nil, 123].each do |item| case item in nil | String p 'nil か String だよ' else p 'nil でも String でもないよ' end end #=> "nil か String だよ" #=> "nil か String だよ" #=> "nil でも String でもないよ"
As パターン
パターン記法: pat => var
値がパターンにマッチした時、値を変数に代入します。
case 0 in Integer => a p a #=> 0 end
複雑な構造のオブジェクトをパターンマッチして、値を取り出す時に便利!
case [0, [1, 2]] in [0, [1, _] => a] p a #=> [1,2] end
case [0, [1, 2]] in [0, [1, Integer => a]] p a #=> 2 end
case [0, [1, 2]] in [0, a] # a はオブジェクトならマッチングする p a #=> [1,2] end
Value パターンと似てるけど、オブジェクトの構造が複雑であるほど真価を発揮しそうですね👀
Array パターン
名前に反して、 Array パターンは配列オブジェクト専用ではありません笑
パターン記法
Constant(pat, ..., *var, pat, ...)
Constant[pat, ..., *var, pat, ...]
[pat, ..., *var, pat, ...] (BasicObject[ ... ] と同義)
Array パターンは以下の場合にパターンマッチします。
Constant === object
がtrue
- オブジェクトに
Array
を返す#deconstruct
が定義されている- 入れ子になったパターンに対して
object.deconstruct
がtrue
Array
クラスを使った Array パターン
class Array def deconstruct self end end 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]
Struct
クラスを使った Array パターン
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] puts "Black" in Color(255, 0, 0) puts "Red" in [0, 255, 0] puts "Green" in r, g, b puts "#{r}, #{g}, #{b}" end #=> 0, 10, 20
Hash パターン
こちらも名前に反して、 Hash パターンはハッシュオブジェクト専用ではありません笑
パターン記法
Constant(id: pat, id:, ..., var)
Constant[id: pat, id:, ..., var]
{id:, id: pat, ..., **var} (BasicObject{ ... } と同義)
Hash パターンは以下の場合にパターンマッチします。
Constant === object
がtrue
- オブジェクトに
Hash
を返す#deconstruct_keys
メソッドが定義されている- 入れ子になったパターンに対して
object.deconstruct_keys(keys)
がtrue
Hash
クラスを使った Hash パターン
class Hash def deconstruct_keys(keys) self end end case {a: 0, b: 1} in Hash(a: a, b: 1) in Object[a: a, b: 1] in {a: a, b: 1} in a: a, b: 1 # `{}` の省略可能 in a:, b: # 値の省略可能 p a #=> 0 p b #=> 1 in {a: a, **rest} p rest #=> {:b=>1} end p a #=> 0
#deconstruct_keys
を誤って実装すると非効率的な結果となることがあるので、たくさんのキーを取りたい場合は愚直に書いた方が早いし見やすくなるとので注意が必要とのことでした😮
使ってみた感想
- Alternative パターンで、
in [a, 2, b] | Integer
のような書き方はできなかった気がする💦- パターンマッチングはまだ実験的なもの👀
- 様々なパターンが用意されているため、条件分岐をスッキリ書くことができそうなので期待✨
- でもパターン、色々組み合わせるってなったら難しそう😂
まとめ
「パターンマッチング使って遊んでみた!」みたいな記事をみなさん書かれていて、みんなパターンマッチングを活用していきたい感を多大に感じ取れます🤗
- Ruby 2.7 で導入される予定の Pattern Matching を触ってみる - Tech Blog by Akanuma Hiroaki
- Ruby の trunk に(実験的に)パターンマッチ構文が入った! - Qiita
- Ruby2.7の(実験的)新機能「パターンマッチ」で遊ぶ - メドピア開発者ブログ
Pattern Matching 試した! dig せず値が取れるぞい pic.twitter.com/5siq0GN1YM
— 🚯りさきゃん📫 (@_risacan_) May 11, 2019
社内でも気になる!って話をたくさん聞いたのは、パターンマッチングに対する期待・可能性を感じている方が多いのだと思います✨
12月での 2.7.0
のリリースが楽しみですね🎁🎄