最近、fluent-plugin-droongaという分散データストリームエンジンを書いています。その中で、RubyでDSLを実現するときに工夫していることに気づきました。それは、値を設定するときは代入する字面にするということです。代入する字面にするために、グループ化用のオブジェクトを作っていました。
これだけだとどういうことかわからないので、具体例を示しながら説明します。
RubyとDSL
Rubyを使っているとRubyで実現されたDSLに触れることが多くあります。RubyのMake実装であるRakeの設定ファイルもそうですし、ライブラリー管理ツールのBundlerの設定ファイルもそうです。
Rakeの場合:
task :test do
ruby("test/run-test.rb")
end
Bundlerの場合:
source "https://rubygems.org/"
gem "rake"
設定ファイルではなく、Rubyのコードの中で使われているDSLもあります。WebアプリケーションのSinatraはWebアプリケーション作成のためのDSLと自称しているくらいです。
Sinatraの場合:
require "sinatra"
get "/" do
cache_control :public, :must_revalidate, :max_age => 60
"Hello world!"
end
いろんなDSLを見るといくつかの種類に分類できることに気づきます。例えば次のように分類できます。
- 定義系:動作に名前をつける。(メソッド定義とかの特化版)
task :test
:タスクを定義get "/"
:「GET / HTTP/1.1」されたときの動作を定義
- 宣言系:登録する。N回実行するとN個登録できる。(
attr_reader
とかの特化版)source "https://rubygems.org/"
:RubyGemsの取得元を宣言gem "rake"
:使うRubyGemsを宣言
- 操作系:実行する。(メソッド呼び出しの特化版)
ruby("test/run-test.rb")
:Rubyでスクリプトを実行
- 設定系:値を変える。N回実行すると最後の値が有効になる。(代入)
cache_control
:Cache-Controlヘッダーの値を設定
今回注目するのは設定系です。設定系のDSLを作るときに工夫していることです。
設定系のDSLは代入する字面にする
設定系は値を変えるのでまさに代入という動作です。そのため、「代入する字面」になるようにします。
Sinatraのcache_control
は、次のように宣言系の字面になっています。
cache_control :public, :must_revalidate, :max_age => 60
そうではなく、「代入する字面」にするということです。「代入する字面」とは、例えば次のようにするということです。
self.cache_control = [:public, :must_revalidate, {:max_age => 60}]
ここで気になるのが「self.
」です。Pythonと違いRubyでは明示的に「self
」を書くことがほとんどありません。明示的に書くときは次のようなケースです。
- クラスメソッドを定義するとき(
class << self
) - 自分の代入メソッドを呼び出すとき
「代入する字面」にすると2番目の「自分の代入メソッドを呼び出すとき」というケースに当てはまるので、明示的に「self
」と書く必要があります。これは、ローカル変数への代入と代入メソッドの呼び出しを区別するためのRubyの制限です。
def xxx=(value)
@xxx = value
end
xxx = "local" # <- ローカル変数への代入
self.xxx = "method" # <- xxx=メソッドの呼び出し
self
はRubyでは不自然な字面1です。self
を使わず、自然な字面にするためにオブジェクトを作ります。例えば次のようにします。
response.cache_control = [:public, :must_revalidate, {:max_age => 60}]
「レスポンスのCache-Controlを設定する」と読めるコードで妙なところはありません。このようにself
ではなく何かオブジェクトを使うように工夫していました。
レシーバーをだれにするか
この例ではレシーバーとしてself
ではなくresponse
を導入しました。このレスポンスをどうやって見つけるか。それを見つけるために、「グループ化」して考えていました。
Cache-Controlの例で言うと、「Cache-Controlは何関連の設定だろう」と考えます。レスポンスのヘッダーの設定なのでresponse
を導入しました。「レスポンス関連の設定」と考えたということです。
グループ化して考えたレシーバーをつけるメリット
レシーバーをつけると記述が長くなります。
cache_control :public, :must_revalidate, :max_age => 60
response.cache_control = [:public, :must_revalidate, {:max_age => 60}]
これをデメリットと考えることもできますが、次のメリットはそのデメリットを上回ります。
- 「代入」の字面なので値を上書きすることが明確になる。
- レシーバーが付加情報になるため、より自己記述的になり、コードの意図が明確になる。
- ただし、適切なグループをレシーバー名にした場合。適当につけるとかえってわかりにくくなることもある。
このことから、設定系のDSLはレシーバー付き代入式にすることをオススメします。このとき、レシーバーには設定対象のグループを表す名前をつけます。
実例:fluent-plugin-droonga
いくつか実例を示します。
まずは、この工夫をしていることを自覚したfluent-plugin-droongaのケースです。
fluent-plugin-droongaは入力メッセージや出力メッセージの処理をプラグインでカスタマイズできます。プラグインは特定のメッセージを処理します。どのメッセージを処理するかをメタデータとして設定するAPIになっています。
module Droonga
module Plugins
module CRUD
class Adapter < Droonga::Adapter
message.input_pattern = ["type", :equal, "add"]
message.output_pattern = ["body.success", :exist?]
def adapt_input(input_message)
end
def adapt_output(output_message)
end
end
end
end
end
message.input_pattern=
とmessage.output_pattern=
が設定系のDSL2です。メッセージ関連の設定なのでmessage
をレシーバーにしています。input_message.pattern=
とoutput_message.pattern=
にしてもよいでしょう。
これを次のようにすることもできます。
class Adapter < Droonga::Adapter
input_pattern ["type", :equal, "add"]
output_pattern ["body.success", :exist?]
end
宣言系のDSLのような字面です。DSLが好きな人はこちらを好むかもしれません。しかし、ここで設定した値を使うときにself
とは違った違和感があります。
設定した値を取得するときは引数なしで同じメソッドを呼びます。
Adapter.input_pattern # -> ["type", :equal, "add"]
Adapter.output_pattern # -> ["body.success", :exist?]
これらのメソッドの実装はこうなります。
class Droonga::Adapter
class << self
PATTERN_NIL = Object.new
def input_pattern(pattern=PATTERN_NIL)
if PATTERN_NIL == pattern
@input_pattern
else
@input_pattern = pattern
end
end
end
end
1つのメソッドで2つのことをしているので理解するまでにワンクッション必要なコードになっています。
message.input_pattern=
の実装はこうなります。
class Droonga::Adapter
class MessageConfiguration
attr_accessor :input_pattern
attr_accessor :output_pattern
def initialize
@input_pattern = nil
@output_pattern = nil
end
end
class << self
def message
@message ||= MessageConfiguration.new
end
end
end
オブジェクトが1つ増えていますが、やっていることは明快なのですぐに理解できます。
実例:milter manager
別の例を紹介します。
設定ファイルで値を設定するケースです。milter managerというソフトウェアの設定ファイルはRubyスクリプトで次のようになっています。
security.privilege_mode = false
security.effective_user = nil
security.effective_group = nil
manager.connection_spec = "inet:10025@[127.0.0.1]"
manager.unix_socket_mode = 0660
manager.unix_socket_group = nil
security
とmanager
をグループとしています。
milter managerもグループごとにオブジェクトを作る実装になっています。
まとめ
Rubyで自然なDSLを作るコツとして、値を設定するときは、レシーバーに設定対象のグループを示す名前をつけ、代入式にするとよいということを紹介しました。実例と実装例もつけたので、よさそうだと思ったらマネしてみてください。
-
Rubyist Magazine - Ruby コードの感想戦 【第 2 回】 WikiR - set_srcのこと参照。 ↩
-
fluent-plugin-droongaに特化したシンタックス。DSLではなく、単なるAPIだよね、といってもいいくらい薄いDSL。 ↩