メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

RubyKaigi 2023 「Reading and improving Pattern Matching in Ruby」 感想

こんにちは、サーバーサイドエンジニアの草分です。
先日のRubyKaigi 2023に参加された皆様お疲れ様でした!

"感想記事を書くまでがRubyKaigi" ということで、今回は1つのセッションを掘り下げた感想記事を投稿します。

rubykaigi.org

このセッションではRubyのパターンマッチの機能を題材に、Rubyの機能の実装を「読んで理解する」そして「パフォーマンスを向上させる」といったプロセスを、いかにして進めていくのか。その方法について紹介されていました。

このセッションはRubyのパターンマッチやメタプログラミングを知った状態で聞くと、より深く理解することができます。 それらの前提知識を軽くおさらいしつつ、セッション内容を振り返っていきましょう。

パターンマッチとは

パターンマッチとは、「データ構造による条件分岐」「構成要素の取り出し」という要素を備えた機能です。

Rubyでは case/in 構文で記述します。
対象のオブジェクトとマッチさせるデータ構造(パターン)を比較し、マッチした場合処理が行われます。

また、マッチした場合はパターンに記載した各変数に値がバインドされ、内部の処理で利用することが可能となります。

User = Struct.new(:role, :name)

def hello(user)
  case user
  in role: 'member', name:
    puts "ようこそ、#{name}さん"
  in role: 'guest'
    puts "ようこそ、ゲストユーザーさん"
  else
    puts "ようこそ"
  end
end

hello(User[:member, "太郎"])  # => ようこそ、太郎さん
hello(User[:guest, "ゲスト"])  # => ようこそ、ゲストユーザーさん
hello(nil)                   # => ようこそ

Arrayパターンについて

複数あるパターンマッチの型の内の1つで、オブジェクトの #deconstruct が配列を返す場合、その配列と指定パターンがマッチするかを検査する機能です。

class Array
  def deconstruct
    self
  end
end

case [0, 1, 2]
  in [0, *a, 2]
    puts a #=> 1
end

※実際にはArray#deconstructはデフォルトで利用可能です

その他のパターンマッチの使い方については過去に解説記事を書いています。 こちらも是非ご覧ください。

tech.medpeer.co.jp

tech.medpeer.co.jp

Arrayパターンを改善していく

Rubyのソースコードは構文解析などによりAST(抽象構文木)に変換され、そこからYARV命令列に変換され実行されます。
実際にソースコードがどう変換されるかは RubyVM::InstructionSequence を使うと簡単に表示することができます。

puts RubyVM::InstructionSequence.compile("puts 2 + 2").disasm
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,10)> (catch: false)
0000 putself                                                          (   1)[Li]
0001 putobject                              2
0003 putobject                              2
0005 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0007 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0009 leave

パターンマッチのソースコードがどういうYARV命令に変換されるかを確認すると… とてつもない量の命令が出力されてしまったとのこと。
(本当に量が多かったので本記事では省略)

落ち着いて、ASTをYARV命令列に変換しているcompile.cファイルを見てみましょう。とのこと。
パターンマッチの部分には素晴らしいコメントが記載されており、理解の手助けになるようです。果たして本当でしょうか。

compile.cは1万行をゆうに超える上に、ASTノードの変換を行う関数 iseq_compile_each0 には100近い分岐のswitch文が存在し、それぞれの処理を行っています。

いやはや、何の知識もない状態で読んでも全く理解できないことでしょう。 実際にファイルを開いてみます。 https://github.com/ruby/ruby/blob/8d242a33af19672977dcdcb8d32e9ad547bc0141/compile.c#L6363

compile.c

おお!読めるぞ!?(一部分のみ)

さすがに実際のコードはすぐに読み取れる訳ではありませんが、 読む範囲が定まっており、読みやすいコメントが記載されていれば、なんだかいけそうな気になりますね!(気のせいかも)

改善方法について

Array patternでパターンマッチする場合、検査対象が#deconstructを持つかどうか、その戻り値がArrayかどうかのチェックが実行されます。
しかし対象がArrayの場合は#deconstructの検証は不要なはず、その処理をスキップすれば速度改善になるのではないか。

といった仮説から改善が行われました。

結果、ベンチマークでは速度が2倍に向上!やった!
しかしながら、Array#deconstructがオーバーライドされているケースで問題があったとのこと。

オーバーライドによる問題点

Rubyには「オープンクラス」という機能があります。 既存のクラスを任意の場所で再オープンし、メソッドの修正や追加を行うことができる機能です。 組み込みクラスのArrayも例外ではなく、既存メソッドの挙動を好き放題書き換えることができます。

オープンクラスについてはメタプログラミングRubyなどの書籍を読むと詳しく理解できるでしょう。

前述の改善方法は、Array#deconstructの内容の検証をスキップするものです。 オープンクラスによりメソッドが書き換えられていた場合、検証をスキップすると期待値と異なる動作をしてしまいますね。

さて、その問題に対してはどう対処するかというと……

Do not override Array#deconstruct !

といったオチに繋がるセッションでした。

感想

コーナーケースへの対処の難しさ

Array#deconstructの上書きですが、普通にRubyを使っている場合はおそらく書かないコードでしょう。
Rubyの利用者からすればあまり考慮する必要はなさそうです。

一方で、Ruby言語としては許されている実装です。
言語の改善のためにはそういったコーナーケースへの対処も必要なようです。

「これらを全て考慮するのは大変そうだぞ……」という気分になってしまいますね。

Array patternはArrayだけのものでない

パターンマッチのArray patternはArrayだけのものではありません。
以下のように、#deconstructが定義済の任意のオブジェクトで利用することが可能です。

Spot = Struct.new(:latitude, :longitude)

def latlng(spot)
  case spot
    in [0.. => lat, 0.. => lng]
      puts "北緯#{lat.abs}度 東経#{lng.abs}"
    in [..0 => lat, 0.. => lng]
      puts "南緯#{lat.abs}度 東経#{lng.abs}"
    in [0.. => lat, ..0 => lng]
      puts "北緯#{lat.abs}度 西経#{lng.abs}"
    in [..0 => lat, ..0 => lng]
      puts "南緯#{lat.abs}度 西経#{lng.abs}"
  end
end

latlng(Spot.new(35, 135))   # => 北緯35度 東経135度
latlng(Spot.new(-35, -135)) # => 南緯35度 西経135度

この場合のパフォーマンスはどうなってしまうのでしょう。 チェック処理が増えた影響はあるのでしょうか。 手元での検証はできていませんが、ひとつの事柄を掘り下げていくと次の疑問点も見えてきますね。

おわりに

RubyKaigiのセッションはRubyの内部に踏み込んだ内容が多く、前提知識がないと理解すらままならないこともあります。 ということで今回は、1セッションの前提知識の内容も含めた記事を投稿いたしました。 今後のRubyKaigiのセッションを理解するための一助になれれば幸いです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp