はじめまして、メドピアのサーバサイドエンジニアの草分です。
RubyKaigi2019 1日目のセッションにてRubyのexperimental(実験的)な新機能「パターンマッチ」(Pattern Matching)が発表されましたね。
この記事では発表で紹介されたパターンマッチの各種機能を確認し、遊んでみます。
概要
Rubyのパターンマッチとは?
Rubyist向けの説明として以下のような説明がなされていました。
- データ構造の評価によるcase/when条件分岐の提供
- 変数への多重代入
実際に例文を見てみましょう。
例文
case [0, [1, 2, 3]] in [a, [b, *c]] p a #=> 0 p b #=> 1 p c #=> [2, 3] end
case/whenではなくcase/inが追加され、caseで指定したオブジェクトとinに記載したデータ構造(パターン)を比較し、マッチした場合処理が行われます。
また、マッチした場合はパターンに記載した各変数に値がバインドされ、内部の処理で利用することが可能となります。
通常の多重代入とは異なり、オブジェクトのデータ構造とパターンが一致しない場合は処理されません。
case [0, [1, 2, 3]] in [a] :unreachable # 配列構造が異なるため処理されない in [0, [a, 2, b]] p a #=> 1 p b #=> 3 end
その他、Hashもサポートされています。
case {a: 0, b: 1} in {a: 0, x: 1} :unreachable # Hashの構造が異なるため処理されない in {a: 0, b: var} p var #=> 1 end
それではこの機能が実装されているRubyを導入し、実際に試してみましょう。
準備
最新のRubyを導入する
- まずは最新のRubyのソースコードをcloneし、trunkをチェックアウトします。
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ git checkout origin/trunk
- 公式の手順に従ってRubyをビルドします。
$ autoconf $ ./configure $ make $ make install
- しばらく待てば最新Rubyの完成です。
- 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 :unreachable # ここは処理されない end
パターンマッチの基本文法は以上ですが、実行するとwarning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
という警告が表示されます。
警告の通り、パターンマッチは実験的な機能であり将来振る舞いが変更される可能性があります。ご注意ください。
次は、現時点で利用可能なパターンの記法を1つずつ確認してみましょう。
Value pattern
case 0 in 0 in -1..1 in Integer end
===
で比較して一致した対象にマッチします。- そのため、上記3パターンはいずれもマッチさせることができます。
- ※処理が実行されるのは最初にマッチしたパターンのみです。
Variable pattern
case 0 in a puts a #=> 0 end
- 任意の値にマッチし、マッチした値はローカル変数としてバインドされます。
case [0, 1] in [_, _] :reachable end
- また、変数として利用しない場合は
_
を使うことができます。
Alternative pattern
case 0 in 0 | 1 | 2 :reachable end
|
を用いることで複数のパターンにマッチさせることができます。
As pattern
case 0 in Integer => a a #=> 0 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のパターンマッチから見ていきましょう。
class Array def deconstruct self end end case [0, 1, 2] # ↓4種のいずれの記法でもマッチ可能 in Array(0, *a, 2) in Object[0, *a, 2] in [0, *a, 2] in 0, *a, 2 # []は省略可能 end
次はStructを対象としたArray patternのパターンマッチを見てみましょう。
class Struct alias deconstruct to_a end Color = 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名を指定してパターンマッチを行うことができます。
class Hash def deconstruct_keys(keys) self end end case {a: 0, b: 1} # ↓4種のいずれの記法でもマッチ可能 in Hash(a: a, b: 1) in Object[a: a] in {a: a} in {a: a, **rest} p rest #=> {b: 1} end
こちらも独自に#deconstruct_keys
を定義することで任意のobjectに対してパターンマッチを行うことができるようになるため、Rubyのパターンマッチの大きな特徴の1つとなっています。
また、#deconstruct_keys
の引数keys
には処理の効率化のためのヒントとして、パターンの処理に必要なkey名の配列が渡されます。
その配列に含まれないkeyについては返却せず無視してよく、処理コストの削減を行うことができます。
遊ぶ
クイックソートしてみる
「クイックソートはパターンマッチを用いると書きやすい」という情報を社内で耳にしたため試してみます。
処理対象は数値の配列であることを前提として、ソート処理を書いてみました。
def qsort(ary) case ary in [piv, *xs] # 要素が2個以上 lt, rt = xs.partition{|x| x < piv} qsort(lt) + [piv] + qsort(rt) else # 要素が0個または1個 ary end end
ピボットの位置は先頭で決め打ちですが、かなりシンプルに書けますね!
逆にパターンマッチを使用せずに書くとすればこんな感じでしょうか。
def qsort2(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と組み合わせて利用しています。
class Time def deconstruct_keys(keys) { year: year, month: month, day: day } end def to_reiwa case self in year: 2019, month: (5..) "令和元年" in year: (2020..) => year "令和#{year - 2018}年" else "not令和" end end end
実行してみます。ちゃんと動きましたね。
p Time.local(2019, 5, 1).to_reiwa # => 令和元年 p Time.local(2019, 8, 1).to_reiwa # => 令和元年 p Time.local(2020, 1, 1).to_reiwa # => 令和2年 p Time.local(2019, 4, 30).to_reiwa # => not令和
引数の型指定をしてみる
当日の発表では「引数に対してパターンマッチを実行可能とするのは技術的には可能だが、型の指定に使われることが想定されるので実装しなかった。」という内容の発言がありました。
ということで言いつけを破ってTryしてみましょう。
def arg(num, str) case [num, str] in [Integer, String] :ok end end
このメソッドを呼び出してみると…
p arg(3, "3") # => :ok p arg("2", 2) # => NoMatchingPatternError p arg(:a, "a") # => NoMatchingPatternError p arg(1, :a.to_s) # => :ok
引数の型を間違えると例外をraiseするメソッドになりました! 全くRubyらしくありませんね。
まとめ
RubyKaigi2019で紹介されたパターンマッチの機能を一通り触ってみました。まだ実験的な機能であるとのことで仕様変更の可能性はありますが、正式リリースが楽しみな機能です。
この他にもRuby3で実装が予定されている静的解析や、逆に搭載が見送られた仕様など、将来のRubyに関する具体的な情報が次々発表されたRubyKaigi2019でした。今後のRubyの動向から目が離せませんね!
(☝︎ ՞ਊ ՞)☝︎是非読者になってください
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら