pumaに学ぶrubyのDSLの作り方

調べごとがあって、pumaのソースを読んだ。

そこでpumaの設定ファイルに書いてあるDSLの適用の処理が勉強になったので書く。

(puma: v2.11.0 時点)

pumaのソース

puma/dsl.rb at master · joe-re/puma · GitHub

ここでやっている。

主要な処理だけ抜き出した

主要な処理だけ抜き出して、少しだけ修正して分かりやすくした。

pumaに学ぶRubyのDSLの作り方

これを実行すると以下になる。

[~/src/sandbox/ruby_dsl]$ ruby dsl.rb
blockも渡せる
#<DSL:0x007fc39a1fc740 @options={:foo=>"test", :boolean_value=>true}>

config.rbで設定した値が適用されたのが分かる。

いいところ

configファイルを間違って記述したときに、分かりやすい形でエラーを返してくれる。

config.rbを書き換える。

foo 'test'

block_test do
  puts 'blockも渡せる'
end

# コメントは無視される

# boolean_test
boolean_toast # typo!

実行する

[~/src/sandbox/ruby_dsl]$ ruby dsl.rb
blockも渡せる
config.rb:10:in `_load_from': undefined local variable or method `boolean_toast' for #<DSL:0x007f91c106c570> (NameError)
...

config.rbの10行目のboolean_toastが違うよ!と言ってくれる。賢い。

Rubyで書ける

DSLとは言え、コメントとかブロック書くときにRubyのルールで書けるのはうれしい。

実装が簡単

分かりやすいメタプログラミング

解説

DSLクラスの_load_formメソッド内のinstance_evalに注目する。

instance_evalはrubyのBasicObjectに定義されているので、全てのオブジェクトで使えるインスタンスメソッドだ。

evalと同様、文字列が渡されたときはそれをRubyのコードとして評価するのだけど、違うのはinstance_evalのレシーバが評価のコンテキストになるということ。

この場合、レシーバを書かずにインスタンスメソッド内で実行しているので、レシーバはselfになる。

下と同じ。

self.instance_eval(File.read(path), path) if path

第1引数には、File.read(path)でファイルを読み込んだ文字列が渡されている。DSLクラスのインスタンスをコンテキストにして、この文字列が評価される。よって、config.rbに書いたDSLと同名のDSLクラスのインスタンスメソッドが呼ばれる。

instance_evalメソッドの第2引数にはファイルパスを渡していて、これのおかげでエラーが分かりやすくなる。

渡したファイルパスで実行されているようにスタックトレースを差し替えてくれるのだ。

instance method BasicObject#instance_eval

終わりに

unicornも読んでみたらほぼ同じ仕組みだった。

デザインパターンなのか?今日学んだパターンの名前を僕はまだ知らない。