はじめまして、メドピアのサーバサイドエンジニアの草分です。
RubyKaigi2019 1日目のセッションにてRubyのexperimental(実験的)な新機能「パターンマッチ」(Pattern Matching)が発表されましたね。
speakerdeck.com
この記事では発表で紹介されたパターンマッチの各種機能を確認し、遊んでみます。
概要
Rubyのパターンマッチとは?
Rubyist向けの説明として以下のような説明がなされていました。
- データ構造の評価によるcase/when条件分岐の提供
- 変数への多重代入
実際に例文を見てみましょう。
例文
case [0, [1, 2, 3]]
in [a, [b, *c]]
p a
p b
p c end
case/whenではなくcase/inが追加され、caseで指定したオブジェクトとinに記載したデータ構造(パターン)を比較し、マッチした場合処理が行われます。
また、マッチした場合はパターンに記載した各変数に値がバインドされ、内部の処理で利用することが可能となります。
通常の多重代入とは異なり、オブジェクトのデータ構造とパターンが一致しない場合は処理されません。
case [0, [1, 2, 3]]
in [a]
:unreachablein [0, [a, 2, b]]
p a
p b end
その他、Hashもサポートされています。
case {a: 0, b: 1}
in {a: 0, x: 1}
:unreachablein {a: 0, b: var}
p var end
それではこの機能が実装されているRubyを導入し、実際に試してみましょう。
準備
最新のRubyを導入する
- まずは最新のRubyのソースコードをcloneし、trunkをチェックアウトします。
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ git checkout origin/trunk
$ autoconf
$ ./configure
$ make
$ make install
- rbenvをお使いの場合、2019年5月時点では
2.7.0-dev
バージョンを用いるとパターンマッチが利用可能なrubyをインストールすることができます。
$ rbenv install 2.7.0-dev
解説
基本文法/仕様
試す前に、パターンマッチの基本文法を確認してみましょう。
case expr
in pattern [if|unless condition]
...
in pattern [if|unless condition]
...
else
...
end
case 句に記載したものを検査対象として、以下のように動作します。
- 記載したパターンを上から順番に評価し、最初にマッチしたものが処理される。
- 1つもマッチしない場合はelse句の内容が処理される。
- 1つもマッチせず、else句も存在しなかった場合は
NoMatchingPatternError
例外が発生する。
また、if/unlessによるguardも可能となっています。
case [1, 1]
in [a, b] unless a == b
:unreachableend
パターンマッチの基本文法は以上ですが、実行するとwarning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
という警告が表示されます。
警告の通り、パターンマッチは実験的な機能であり将来振る舞いが変更される可能性があります。ご注意ください。
次は、現時点で利用可能なパターンの記法を1つずつ確認してみましょう。
Value pattern
case0in0in-1..1inIntegerend
===
で比較して一致した対象にマッチします。- そのため、上記3パターンはいずれもマッチさせることができます。
- ※処理が実行されるのは最初にマッチしたパターンのみです。
Variable pattern
case0in a
puts a end
- 任意の値にマッチし、マッチした値はローカル変数としてバインドされます。
case [0, 1]
in [_, _]
:reachableend
- また、変数として利用しない場合は
_
を使うことができます。
Alternative pattern
case0in0 | 1 | 2:reachableend
|
を用いることで複数のパターンにマッチさせることができます。
As pattern
case0inInteger => a
a end
=>
を用いることでマッチした値を任意の変数にバインドすることができます。- rescueの使い方と似た記法ですね。
rescue StandardError => e
Array pattern
ここから少し複雑になってきます。
Array patternでは以下のいずれかの記法がパターンとして利用できます。
pat: Constant(pat, ..., *var, pat, ...)
| Constant[pat, ..., *var, pat, ...]
| [pat, ..., *var, pat, ...] # BasicObject(...)のシンタックスシュガー
そして以下の条件を満たすとマッチします。
Constant(何らかのclassを指定) === 検査対象object
がtrueを返す- 検査対象objectの
#deconstruct
メソッドが配列を返す - そこで返した配列と指定したパターンがマッチする
この#deconstruct
メソッドの使い方が鍵になります。
まずはArrayを対象としたArray patternのパターンマッチから見ていきましょう。
classArraydefdeconstructselfendendcase [0, 1, 2] in Array(0, *a, 2)
inObject[0, *a, 2]
in [0, *a, 2]
in0, *a, 2end
次はStructを対象としたArray patternのパターンマッチを見てみましょう。
classStructaliasdeconstructto_aendColor = Struct.new(:r, :g, :b)
color = Color[255, 0, 0]
case color
in [0, 0, 0]
puts "Black"in [255, 0, 0]
puts "Red"in [r, g ,b]
puts "#{r}, #{g}, #{b}"end
このように、独自に#deconstruct
を定義することで任意のobjectに対してArray patternによるパターンマッチを行うことができるようになります。
これがRubyのパターンマッチの大きな特徴の1つとなっています。
Hash pattern
Hash patternでは以下のいずれかの記法がパターンとして利用できます。
pat: Constant(id: pat, id:, ..., **var)
| Constant[id: pat, id:, ..., **var]
| {id:, id: pat, **var} # BasicObject(...)のシンタックスシュガー
そして以下の条件を満たすとマッチします。
Constant(何らかのclassを指定) === 検査対象object
がtrueを返す- 検査対象objectの
#deconstruct_keys
メソッドがHashを返す - そこで返したHashと指定したパターンがマッチする
前述のArray patternと似た形式で、Hashのようにkey名を指定してパターンマッチを行うことができます。
classHashdefdeconstruct_keys(keys)
selfendendcase {a: 0, b: 1} in Hash(a: a, b: 1)
inObject[a: a]
in {a: a}
in {a: a, **rest}
p rest end
こちらも独自に#deconstruct_keys
を定義することで任意のobjectに対してパターンマッチを行うことができるようになるため、Rubyのパターンマッチの大きな特徴の1つとなっています。
また、#deconstruct_keys
の引数keys
には処理の効率化のためのヒントとして、パターンの処理に必要なkey名の配列が渡されます。
その配列に含まれないkeyについては返却せず無視してよく、処理コストの削減を行うことができます。
遊ぶ
クイックソートしてみる
「クイックソートはパターンマッチを用いると書きやすい」という情報を社内で耳にしたため試してみます。
処理対象は数値の配列であることを前提として、ソート処理を書いてみました。
defqsort(ary)
case ary
in [piv, *xs]
lt, rt = xs.partition{|x| x < piv}
qsort(lt) + [piv] + qsort(rt)
else
ary
endend
ピボットの位置は先頭で決め打ちですが、かなりシンプルに書けますね!
逆にパターンマッチを使用せずに書くとすればこんな感じでしょうか。
defqsort2(ary)
return ary if ary.length <= 1
piv = ary[0]
lt, rt = ary[1..].partition{|x| x < piv}
qsort2(lt) + [piv] + qsort2(rt)
end
比べてみると、配列長の確認や配列の分割が冗長に見えてきますね。
オブジェクト構造による条件分岐と変数へのバインドが同時に行えるパターンマッチは、かなり強力な文法なのではないでしょうか。
独自拡張クラスを作ってみる
Hash patternをHash以外で使ってみたかったので、Timeクラスを独自拡張し、令和表記で年号を返すメソッドを作ってみました。
As patternと組み合わせて利用しています。
classTimedefdeconstruct_keys(keys)
{ year: year, month: month, day: day }
enddefto_reiwacaseselfinyear: 2019, month: (5..)
"令和元年"inyear: (2020..) => year
"令和#{year - 2018}年"else"not令和"endendend
実行してみます。ちゃんと動きましたね。
p Time.local(2019, 5, 1).to_reiwa
p Time.local(2019, 8, 1).to_reiwa
p Time.local(2020, 1, 1).to_reiwa
p Time.local(2019, 4, 30).to_reiwa
引数の型指定をしてみる
当日の発表では「引数に対してパターンマッチを実行可能とするのは技術的には可能だが、型の指定に使われることが想定されるので実装しなかった。」という内容の発言がありました。
ということで言いつけを破ってTryしてみましょう。
defarg(num, str)
case [num, str]
in [Integer, String]
:okendend
このメソッドを呼び出してみると…
p arg(3, "3")
p arg("2", 2)
p arg(:a, "a")
p arg(1, :a.to_s)
引数の型を間違えると例外をraiseするメソッドになりました!
全くRubyらしくありませんね。
まとめ
RubyKaigi2019で紹介されたパターンマッチの機能を一通り触ってみました。まだ実験的な機能であるとのことで仕様変更の可能性はありますが、正式リリースが楽しみな機能です。
この他にもRuby3で実装が予定されている静的解析や、逆に搭載が見送られた仕様など、将来のRubyに関する具体的な情報が次々発表されたRubyKaigi2019でした。今後のRubyの動向から目が離せませんね!
(☝︎ ՞ਊ ՞)☝︎是非読者になってください
メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら
https://medpeer.co.jp/recruit/workplace/development.html