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の代わりの”=>”やパターンとして使っている”=()”は、パースが簡単そうというだけの理由で選んでおり強い思いはない。