RubyKaigi 2017に参加してきました。
今回は例年にも増して濃い感じのタイムスケジュールとなっていましたが、 Rubyへのパターンマッチ導入を目論んでいる身としてはその中でも@yotii23さんによるPattern Matching in Rubyが大変楽しかったです。Rubyの主要なカンファレンスにおいて、 Ruby本体にパターンマッチ用の構文をいれようという話が実装を伴って出てきたのは今回が初めてのはずで、それ自体意義深い事だったように思います。
提案されていた%pについては、Rubyistにとって馴染み深い文法というのは利点といえる半面、 裏を返すと馴染み深すぎてユーザに誤った印象を与えてしまいそう(オブジェクトに見えるので変数に代入したりすることができそうに思える)とも感じました。 この辺のバランスは難しいですね。
さて、発表に感化されて自分なりにプロトタイプを作ってみました。まだまだ荒削りですが、いずれFeatureチケット化までこぎ着けたいところです。
# caseバージョン case obj => =(pat, ...) [if|unless cond] ... => =(pat, ...) [if|unless cond] ... end # 代入バージョン =(pat, ...) [if|unless cond] = obj # パターン pat: var # 任意のオブジェクトにマッチし、varを束縛 | _ # 任意のオブジェクト | literal # ===メソッドの実行結果が真になるオブジェクト | Constant # 同上 | var_ # 同上(Elixirのピン演算子相当) | !pat # patにマッチしないか | pat && pat # 両方のpatにマッチするか | pat || pat # どちらかのpatにマッチするか | =(pat, ..., *var, pat, ..., id:, id: pat, ..., **var) # ScalaのExtractor相当(Rubyのメソッド仮引数とほぼ同等構文)
# caseバージョン class Object def deconstruct self end end class C def deconstruct [0, 1, [2, 1], x: 3, y: 4, z: 5] end end case C.new => =(0, *, =(b && Integer, _), x: 3, y:, **z) if b == 2 p b #=> 2 p y #=> 4 p z #=> {z: 5} end # 代入バージョン =(x:, =(y:)) = {x: 0, {y: 1}} p x #=> 0 p y #=> 1
Ruby版IOCCCであるところのTRICK 2015というコンテストで審査員賞"Matz Lisp award"をいただきました。
受賞コードを抜粋するとこんな感じ。
(DEFINE (FACT N) . ( (IF (EQ? N . (1)) . ( 1 [* N . ((FACT (- N . (1))))])))) (DISPLAY (FACT 6))
よく訓練されたRubyistにとってはただのRubyのコードにしか見えないかもしれませんが、 一応これはSchemeとしても有効なコードになっていて、実行すると6!を計算して出力します。 また、任意のプログラムを外部から与えることができるようにもしていて、 その一例としてSICPに出てくる超循環評価機をポーティングしています(metacircular.rb)。
ということで、今回の作品は一言でいうと「PolyglotなSchemeサブセットのインタプリタ」でした。
Schemeにおいて、シンボルが大文字小文字を区別しないのはR5RSまで、[]を()と同様に扱うようになったのはR6RSからなので より正確には「R5RSサブセット+一部R6RS拡張」になります。 シンボルを大文字にしたのはなるべくオリジナルに忠実に超循環評価機のポーティングをしようとしたため*1ですが、 原理的には小文字にすることも可能です。純粋なR6RSサブセットをお求めの方のために、小文字バージョンも用意しておきました(entry-lowercase.rb)。
実装のポイントはobj.callのシンタックスシュガーobj.()の活用です。remarksから引用します。
リスト((A) B)はこのままだとRubyでsyntax errorになりますが、 ドット対表記((A) . (B))にすると((A).call(B))と解釈されパーサを通るようになります。
これを利用してRuby/Schemeの両方で有効なコードを用意し、 実行時にmethod_missing/const_missingなどを使いながら構文木を組み立ててSchemeプログラムとして評価しています。
基本的にはこれですべてですが、Schemeにおける`FOO'と`(FOO)'はRubyのコードとしてパースすると違いがないので、後者は(FOO . ()))と書くことにしようといったルールを決めていかないといけないのがちょっと大変でした。
また、Refinementsも使っていて(metacircular.rb#L7)、RubyコードとSchemeコードが無理なく共存できるようになっています。ただ、Object.const_missingはRefinementsが有効にならないという仕様があり*2、これだけはグローバルに影響が出てしまうのが心残りです。
実はここまで書いたその日の夜にmethod_missingでRefinementsが有効になるのはバグだとの判断がありプログラムが動かないようになっていました*3。 確かにRefinementsの仕様をよく見てみるとIndirect method accessesではRefinementsを有効化しないという記載があるためこの変更には納得するところで、図らずもTRICKの理念の一つである「Rubyの仕様を安定化する」ことに貢献できた感があります。
なお、GitHub上のコードはRefinementsを使わずにモンキーパッチをあてるように修正したので、少なくとも当面は動いてくれるはずです。
「Power Assert in Ruby」というタイトルで発表してきました。まさかRubyKaigiの壇上に立つ日が来るとは。光栄な話です。
以下、振り返り。
最後に。今回、スピーカーということでスタッフの方の活動を目にする機会が多かったのですが精力的な仕事ぶりに圧倒されました。スタッフの皆さん、本当にありがとうございました。
*1 悩むだけ悩んで結局当初の構成ほぼそのままだったんですが、それなりに好評だったようで安心しました。