トップ 追記 RSS feed

継続にっき

2004|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|12|
2006|01|03|05|06|08|09|10|12|
2007|01|02|03|05|07|12|
2008|10|
2009|01|05|12|
2010|04|05|11|
2011|01|09|12|
2012|02|03|05|09|12|
2013|02|03|
2014|05|09|
2015|12|
2017|09|

2017-09-21 (Thu)

))) Rubyへのパターンマッチ構文導入に向けた一提案

Pattern Matching in Ruby - RubyKaigi 2017

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の中で特別扱いされることが多いArrayとHashに対してパターンが書きやすくなっているというのが利点。
    • 反面、HashのキーとしてSymbolしか利用できなくなっている。
      • Stringを指定したいというユースケースがどの程度あるかがポイントか。
      • ユースケースとして多そうなのはJSONだが、JSON.parseにはsymbolize_namesというオプションがあるため問題になりづらいのでは。
  • 任意のオブジェクトに対するパターンマッチを可能にしたいので、ScalaのExtractor類似のプロトコルを組み込む。
    • パターンマッチ対象のオブジェクトに対してdeconstructメソッドを呼んだ結果に対してマッチさせる。
  • Destructuring assignmentを実現したいので、1行でのパターンマッチに特化した文法(代入バージョン)もサポートする。
  • パターンマッチとセットで語られる事が多い話題としてマルチディスパッチがあるが、言語に対する影響が大きすぎる気がして個人的には否定的。
  • whenの代わりの"=>"やパターンとして使っている"=()"は、パースが簡単そうというだけの理由で選んでおり強い思いはない。

2015-12-12 (Sat)

))) TRICK 2015で入賞した

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、これだけはグローバルに影響が出てしまうのが心残りです。

追記(2015/12/23)

実はここまで書いたその日の夜にmethod_missingでRefinementsが有効になるのはバグだとの判断がありプログラムが動かないようになっていました*3。 確かにRefinementsの仕様をよく見てみるとIndirect method accessesではRefinementsを有効化しないという記載があるためこの変更には納得するところで、図らずもTRICKの理念の一つである「Rubyの仕様を安定化する」ことに貢献できた感があります。

なお、GitHub上のコードはRefinementsを使わずにモンキーパッチをあてるように修正したので、少なくとも当面は動いてくれるはずです。

*1 小文字だとifなどがRubyの文法と衝突してしまう

*2 Bug #11655: Can't refine Object.const_missing

*3 Bug #11809: method_missing should not be refined


2014-09-20 (Sat)

))) RubyKaigi 2014 まとめ

「Power Assert in Ruby」というタイトルで発表してきました。まさかRubyKaigiの壇上に立つ日が来るとは。光栄な話です。

以下、振り返り。

  • RubyKaigi当日まで
    • CFPの締め切り1週間ぐらい前にpower_assertがtest-unitに組み込まれることになったので勢いでsubmitボタンを押す。
    • 準備しだすも誰得な話にしかならないような気がしてきて延々とスライドの構成に悩む。 *1
    • Rebuild.fmのa_matsudaさん回を聞いたことで、追い打ちのようにハードルが3段階ぐらい上がる。
  • 1日目
    • nagachikaさん、ささださん、nariさんと冒頭から連続3セッションで名前を出してもらったことで謎の達成感を得る。
    • Hall-Aが期待を裏切らない濃さで満足。
    • Ruby committers vs the Worldでマイクが回ってくる。パターンマッチ入れたいですよね。
  • 2日目
    • MatzのキーノートはRuby 3.0の話。あれ、パターンマッチは…?
    • Power Assert in Ruby
      • 会場の反応がよくてスピーカーとしては大変やりやすかったです。来ていただいた方、ありがとうございました。
      • 「TracePoint使っている人」と聞いて手が上がったのは10名強というところ。このセッションをきっかけに利用者が増えるとうれしいです。
      • akrさんのメソッド再定義すればいいんじゃないかという指摘は目から鱗だった。なるほどなあ。
        • アトミックに出来るかどうかがまずはポイントかな。
        • 影響範囲を限定するためにRefinementsと組み合わせるということを考えてみたけどこれは微妙?
      • コアに欲しい機能は、ということでいくつか。
        • TracePoint経由で位置情報を取得。
        • method_missingが呼ばれたときにオリジナルのメソッドのidを取得する手段。
        • JavaScriptのarguments相当。
          • これがあると差分比較まで行える。
        • Procのソースコードを取得する手段。
          • PowerPをgemにしていないのはこれが理由で、ruby -e 'p { foo }'などとされると対応しようがない。
    • 夜は#unagiawardへ。yharaさん曰く「神々の集い」。鰻が運ばれてくるまでの間に2.0.0-p576/2.1.3がリリースされている辺りさすがというほかない。
    • #unagiaward解散後にMatz達と合流することになったものの風邪気味だった体調が悪化してきたので退散。その後も面白い話があったようで残念すぎる。
  • 3日目
    • 午後からふらーっと行ってコードを書いたり。

最後に。今回、スピーカーということでスタッフの方の活動を目にする機会が多かったのですが精力的な仕事ぶりに圧倒されました。スタッフの皆さん、本当にありがとうございました。

*1 悩むだけ悩んで結局当初の構成ほぼそのままだったんですが、それなりに好評だったようで安心しました。


2004|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|12|
2006|01|03|05|06|08|09|10|12|
2007|01|02|03|05|07|12|
2008|10|
2009|01|05|12|
2010|04|05|11|
2011|01|09|12|
2012|02|03|05|09|12|
2013|02|03|
2014|05|09|
2015|12|
2017|09|
トップ 追記 RSS feed