先日福岡で開催された RubyKaigi 2019 に参加してきました。下記の辻本さんのセッションの中で Ruby 2.7 で導入される予定のパターンマッチングについての紹介があったので、セッションの資料に沿って触ってみました。
Pattern matching - New feature in Ruby 2.7 - RubyKaigi 2019
セッションの資料は SpeakerDeck にアップロードされているのでそちらを参照させていただきました。
speakerdeck.com
今回使用しているサンプルコードは基本的には上記の資料内のサンプルをそのまま実行しているか、若干変更したものを使用しています。
パターンマッチングとは
パターンマッチングについてのRubyist向けの説明としては、 case/when + multiple assignment
という感じになり、正規表現とかではなくオブジェクトの構造のパターンのマッチングです。下記 issue にて開発が進められています。
bugs.ruby-lang.org
演算子
パターンマッチング用の演算子としては case が拡張され、 case/when だけでなく case/in が追加されています。
case [0, [1, 2, 3]]
in [a, [b, *c]]
p a
p b
p c
end
ちなみに上記のようにパターンマッチングを使用すると、現状は開発中の機能であり、将来的に仕様が変更になるかもしれないということで、下記のように warning が表示されます。
warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
multiple assignment との違いとして、オブジェクトの構造もチェックされ、マッチした部分の変数に値が代入されます。
case [0, [1, 2, 3]]
in [a]
:unreachable
in [0, [a, 2, b]]
p a
p b
end
対象のオブジェクトとして Hash もサポートされています。
case {a: 0, b: 1}
in {a: 0, x: 1}
:unreachable
in {a: 0, b: var}
p var
end
JSON形式のデータを扱う際に、期待した構造になっているかチェックし、なっていた場合には値を取り出すという使い方をする時に便利です。
require 'json'
json = '{
"name": "Alice",
"age": 30,
"children": [
{
"name": "Bob",
"age": 2
}
]
}
'
case JSON.parse(json, symbolize_names: true)
in {name: "Alice", children: [{name: "Bob", age: age}]}
p age
end
上記の case 文と同様のことをパターンマッチングなしでやろうとすると、下記のように書く必要があります。
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]
end
end
仕様 (2019.4.20時点)
確認した 2019/4/20 時点での仕様としては下記のようになっています。
case obj
in pat [if|unless cond]
...
in pat [if|unless cond]
...
else
...
end
- 条件にマッチするまでシーケンシャルに評価が行われる
- マッチする条件がなければ else 句が実行される
- マッチする条件がなく、 else 句も定義されていない場合、 NoMatchingPatternError が raise される
ガード条件も使用可能で、 in 句の条件にマッチした場合にガード条件が評価されます。
case [0, 1]
in [a, b] unless a == b
:reachable
end
パターン
現状パターンとしては下記の6パターンがあります。
- Value pattern
- Variable pattern
- Alternative pattern
- As pattern
- Array pattern
- Hash pattern
それぞれを簡単に見ていきます。
Value パターン
pattern と object を比較し、 pattern === object
であればマッチします。
pat: literal
| Constant
下記の in 句はいずれもマッチします。
case 0
in 0
in -1..1
in Integer
end
Variable パターン
任意の値とマッチし、変数にその値を代入します。
pat: var
case 0
in a
p a
end
変数に代入する必要がない場合はアンダースコアを使うことができます。
case [0, 1]
in [_, _]
:reachable
end
case/in の外側に同じ変数名が使われていても、上書きしてアサインします。
a = 0
case 1
in a
p a
end
既存の変数の値をパターンマッチに使いたいときは、 ^
を使用します。
a = 0
case 1
in ^a
:unreachable
end
Alternative パターン
OR条件でのパターンマッチです。
pat: pat | pat | ...
case 1
in 0 | 1 | 2
p :reachable
end
As パターン
値がパターンにマッチした時に、値を変数に格納します。
pat: pat => pat
case 0
in Integer => a
p a
end
複雑なオブジェクトのパターンマッチで、そのオブジェクトの一部を取り出すのに便利です。
case [0, [1, 2]]
in [0, [1, _] => a]
p a
end
Array パターン
配列オブジェクトに対してのマッチングです。アスタリスク *
で複数の要素にマッチします。下記の in句はいずれもマッチします。
case [0, 1, 2, 3]
in Array(0, *a, 3)
in Object[0, *a, 3]
in [0, *a, 3]
in 0, *a, 3
end
p a
Struct に対してのマッチングも可能です。
class Struct
alias deconstruct to_a
end
Color = Struct.new(:r, :g, :b)
p Color[0, 10, 20].deconstruct
color = Color.new(255, 0, 0)
case color
in Color[0, 0, 0]
puts "Black"
in Color[255, 0, 0]
puts "Red"
in Color[r, g, b]
puts "#{r}, #{g}, #{b}"
end
下記は RubyVM::AbstractSyntaxTree::Node を使った Array パターンの例です。RubyVM::AbstractSyntaxTree を使った Power Assert の実装サンプルになっています。
class RubyVM::AbstractSyntaxTree::Node
def deconstruct
[type, *children, [first_lineno, first_column, last_lineno, last_column]]
end
end
ast = RubyVM::AbstractSyntaxTree.parse('1 + 1')
p ast.type
p ast.children
p ast.deconstruct
node = RubyVM::AbstractSyntaxTree.parse('assert { 3.times.to_a.include?(3) }')
pp node
case node
in :SCOPE, _, _, [:ITER, [:FCALL, :assert, _, _], body, _], _
pp body
end
Hash パターン
Hash パターンと言っていますが Hash オブジェクト以外にも使われるパターンです。下記条件を満たすとマッチします。
- Constant === object が true を返す
- object が Hash を返す #deconstruct_keys メソッドを持っている
- object.deconstruct_keys(keys) への nested pattern の適用結果が true を返す
Hash オブジェクトでのパターンマッチング例は下記の通りです。下記の in句は全てマッチします。
class Hash
def deconstruct_keys(keys)
self
end
end
case {a: 0, b: 1}
in Hash(a: a, b: 1)
in Object[a: a]
in {a: a}
in {a: a, **rest}
p rest
end
中括弧は省略可能です。また、 a:
は a: a
のシンタックスシュガーです。
case {a: 0, b: 1}
in a:, b:
p a
p b
end
keys
は効率的な実装のためのヒントになる情報を提供します。また #deconstruct_keys メソッドの見当違いな実装は非効率的な結果となることがあります。
class Time
def deconstruct_keys(keys)
{
year: year, month: month,
asctime: asctime, ctime: ctime,
yday: yday, zone: zone
}
end
end
case Time.now
in year:
p year
end
keys
はパターンの中で指定された key を含む配列を参照します。 keys
に含まれない key は無視することができます。もしパターンの中で **rest
が指定されている場合、代わりに nil が渡されます。その場合には全てのキーバリューペアを返さなければなりません。
class Time
VALID_KEYS = %i(year month asctime, ctime, yday, zone)
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,
asctime: asctime, ctime: ctime,
yday: yday, zone: zone
}
end
end
end
now = Time.now
case now
in year:
p year
end
まとめ
今回はとりあえず紹介されているサンプルで、 Ruby2.7 で実装される予定の Pattern Matching を触ってみました。仕様はまだ変わる可能性がありますが、 JSON オブジェクトに対してのマッチングは直感的に指定することができそうで、かつ値の取り出しも同時にできるということで、便利に使えそうです。 RubyKaigi をきっかけに、こうした新しい機能もできるだけ追いかけていきたいと思います。