東京 Ruby 会議 11 直前特集号

Re: Pattern Matching in Ruby

Rubyでパターンマッチを実現するためのライブラリであるpattern-matchに関する発表です。

本ライブラリについてはSapporo RubyKaigi 2012のPattern Matching in Rubyというセッションで機能概要を紹介したことがありますが、今回はメタプログラミングやRefinementsといったRubyならではの表現力をフル活用している実装面についてその舞台裏をお見せします。

必要となる知識

ScalaやHaskellにおけるパターンマッチの機能を知っていると内容が理解しやすいと思いますが必須ではありません。

辻本 和樹
株式会社野村総合研究所

Rubyコミッタ/power_assert開発者。いわゆるSIer業務を昼間の仕事としている一介の趣味プログラマ。


5/15 チャットにて伺いました(話し手:辻本さん、聞き手:笹田)。

笹田 お忙しい中、お時間をとって頂いてありがとうございます。今回のご発表は、Ruby におけるパターンマッチの実装ということで、patten-match gem のお話ということですが、以前、ご発表されていませんでしたっけ。

辻本 札幌 Ruby 会議 2012ですね。

笹田 GitHub を見ると、最後の更新が 2015 年。

辻本 一通り実装したい機能は実装しちゃったんですよね。札幌での発表のときは正規表現相当のマッチは出来なかったんですが、その後にサポートしました。あと、バックトラックもかな。機能面については札幌での発表であらかたカバーしているので、実装の仕方を主眼に発表しようと考えています。加えて、ちらほらpattern-matchを使っている事例が出てきているのでその紹介とか。

pattern-match gem の紹介

笹田 では、pattern-match gem について、ご紹介頂けないでしょうか。

辻本 はい。https://github.com/k-tsj/pattern-match#pattern-match を読んで頂くのが一番はやいですが、主に、matchwith というメソッドで、パターンマッチを書くことができます。このとき、普通の case/when に比べて、

  1. マッチの条件につかうことができる表現を拡張した(=== 以外を許す、and/or/notQuantifierSequence、ガード)
  2. マッチしたときに、オブジェクトの情報を解体し、その情報を使って処理が出来るようにした(deconstruct メソッドというプロトコルを定義した)

というものになります。

笹田 具体的には、マッチはどんなふうにされるんでしょうか。

辻本 基本は === でマッチしますが、_(Fixnum, :==) のように、マッチのためのメソッドを変更することができます。

笹田 なるほど。マッチするところは、case/when と大きな違いはないんですね。

辻本 はい。そこはcase/when に合わせました。

笹田 で、次は解体(deconstruct)ですが、https://github.com/k-tsj/pattern-match#deconstructor を見ているのですが、よくわかりません。普通に [0, 1] とマッチさせるのと何が違うんでしょうか。

辻本 これは、次の Variable のところと一緒に見てもらうとわかります。

match([0, 1]) do
  with(_[a, b]) { [a, b] } #=> [0, 1]
end

このように書くと、a, b に、マッチした 01 がそれぞれ割り当てられて、使うことができます。

笹田 method_missing で無理矢理やってる感じですか。

辻本 その通りです。match に渡したブロックは BasicObject.new.instance_eval のコンテキストで評価されて、method_missing で未定義のメソッドはすべて変数扱いにしています。どのように分解するか、はクラス (オブジェクト) ごとに deconstruct メソッドで決めます。

笹田 _ が色々出てきますね。

辻本 はい、いろいろ意味を持たせてしまっています。今は3つですね。このうち、Array.() のシンタックスシュガー扱いとしている _[] はリテラルの [] との違いが分かりづらく典型的な落とし穴になっています。例えば、https://github.com/k-tsj/pattern-match/issues/3

笹田 マッチの条件では、andor が使える、と。色々リッチになっていますね。

辻本 はい。& とか |Scheme のパターンマッチライブラリ の影響を受けています 。正規表現は Mathematcia 由来といえるのかな (あまり Mathematica 知らないので適当ですが)。

笹田 deconstruct について、もう少し教えて貰えますか。クラスごとに、いろんな選択肢があると思うのですが、例えば、class Foo; attr :bar, :baz; end というクラスがあったとき、foo.bar が配列で、その3要素目、とかを取り出したい、みたいなのも書けますか。

辻本 ちょっと Foo の例を書いてみます。

def Foo.deconstruct(obj)
  accept_self_instance_only(obj)
  [obj.bar, obj.baz]
end

Deconstructはこれだけ。とりあえず自身を分解して配列で返せば OK です。 その上で、match(foo){with(Foo.(_[_,_,a], _)) とやってマッチさせます。

笹田 baz, bar の順序でマッチさせたくても駄目でしょうか。名前の情報が落ちているので。

辻本 それはだめですね。

笹田 インスタンス変数だと、順番覚えろ、はつらいような気がします。

辻本 そこで、こういうことが出来るようにしてはいます。

class Foo
  include PatternMatch::AttributeMatcher
end
match(foo) do
  with(Foo.(bar: _[_,_,a])) { a }
end

笹田 Hash も同じように順番を区別しないでできるようですが、これらは頑張って matcher 書いてる感じですか。

辻本 そうですね。単純な deconstruct より一段上のレベルでの matcher です。

笹田 Deconstruct のプロトコルが難しそうですが、このアイデアはご自身で考えられたんですか?

辻本 今のプロトコルは Scala の unapply という仕組みを割とそのまま持ってきていて、あまり独自のことは考えていません。ただ、現状の仕様で、自分の使う範囲では、今の機能で困ったことはないですね。

pattern-match gem の実装と Ruby への輸入

笹田 ありがとうございました。gem の使い方について、だいたいわかった気がします。さらに、これをどうやって実装するのか、というのは、あまり考えたくないですが。現在は Ruby の機能だけで作っているんですよね。「Ruby 拡張してインタプリタいじったほうが簡単そう!」という印象です。実装するにあたって、Ruby の機能に不足はありませんでしたか。

辻本 はい、裏側を見ると分かるのですがかなり無理矢理動かしている感があるので本体に機能が入るとうれしいなと思っています。文法的には、つまり見た目としては、今ので結構満足しています。Ruby と同じくオブジェクト指向言語でパターンマッチを持っている Scala のそれと比べてもそんなに違和感なく、書くのもつらくない。

笹田 case/when の拡張みたいな話にはならないんでしょうか。

辻本 shugo さんもそんな提案 をされていたはずです。この gem は proof of concept の域を出ないので、Ruby 自体にこの機能を持って行くにしても、今のアルゴリズムはそのままでは移植できないと思います。

笹田 ちなみに、この gem、実際使ってます?

辻本 power_assert の初期実装では便利に使っていました。

笹田 今は使ってないんですか。

辻本 はい。当時は pattern-match が refinements 使っていなかったこともあって Rack の #call とバッティングするなどの問題があったので。

笹田 実装的な問題があったんですね。

辻本 あとやっぱり遅いですね。最近まじめに power_assert の高速化をしていたんですが、TracePoint のイベント呼び出しだけで 3 倍とか時間かかるので…。

笹田 この機能を Ruby 本体に入れる、という話ですと、まつもとさんへの説得材料って揃ってますかね?

辻本 一言で言うと、無くは無いがまだ弱い、というところかなと。ユースケースを示すことが大事だと思うんですが、実際の使われ方を見ていると結局構文解析とか、ごく限られた領域にとどまっているんですよね。Rails で頻出のこのパターンが便利に書ける、みたいなのがあれば通しやすいのでしょうけど。

笹田 それは、知名度の問題じゃないのかな?

辻本 それも大いにあるかもしれません。

笹田 こういうのを知っている人は、たいてい構文解析とかやりたい人達だ、とか。パターンマッチがあれば、例えば、インスタンス変数の分解とか、さっとやりたい、ってのはあると思います。他の例だと、今だと、ハッシュの key、 a, b に対する要素をとるのに2行かかりますが、これを使うと、1行で(match いれると2行か)いける、とか。

辻本 Hash は、別の提案がありますね。

笹田 パターンマッチに一般化したほうが綺麗だと思うんだけど。

辻本 はい、私もそう思います。

笹田 いろんな方法がありますよね。(1) case/when を拡張する (2) 多重代入を拡張する (3) =~ を拡張する (4) それらを混ぜる。

辻本 あと、本体に入れるにしてもパターンマッチそのもので無く、パターンマッチを実装できるプリミティブを入れるという考え方もあって。真っ先に思いつくのはマクロですが、そこまで行かなくてもなんかないかなぁとか。

笹田 マクロはないんじゃないですかね(まつもとさんが嫌がりそう)。

辻本 実際マクロはないでしょうね。

笹田 個人的には、パターンマッチのある言語をがっつり使ったことは無いのですが、あると便利そうだなぁ、とは思います。ぜひ東京 Ruby 会議 11 でまつもとさんを説得して下さい。彼も、欲しいと言っていたと思います。

辻本 頑張ります。

笹田 では、今日はどうもありがとうございました。