RubyでのDSLの作り方

Jay Fields' Thoughts: Implementing an internal DSL in Ruby

RubyでのDSLの作り方をexpectationsというtesting frameworkを例にとって説明している。
やっぱassert_equalってオブジェクト指向っぽくなくてカッコ悪いよねwここはDSLでカッコよく書きたいものだ。

彼の言いたいことを日本語で超要約してみる。

まず、どういうふうに書きたいかを決める。こんな感じにしたい。

Expectations do        # テストだよというブロック
  expect :expected do  # :expectedが期待される値
    :expected          # ブロックの評価結果が実際の値
  end
end

これから、これをRubyスクリプトとして実行可能な形式にする。

まず、DSLを読み込んだら(実行したら)、Expectations::Suiteオブジェクトが作成されるかを確認する。

require 'singleton'
module Expectations; end

class Expectations::Suite
  include Singleton
end

def Expectations(&block)
  Expectations::Suite.instance
end

Expectations do
  expect :expected do
    :expected
  end
end

Expectations::Suite.instance # => #<Expectations::Suite:0x1d4fc>

よし、たしかにできている。次に、DSLを実行したらExpectations::Suiteに格納されるExpectationの個数を確認する。

require 'singleton'
module Expectations; end

class Expectations::Suite
  include Singleton
  attr_accessor :expectations

  def initialize
    self.expectations = []
  end

  def expect(expected, &actual)
    expectations << :expectation      # ここは仮実装!
  end

end

def Expectations(&block)
  Expectations::Suite.instance.instance_eval(&block)
end

Expectations do
  expect :expected do
    :expected
  end
end

Expectations::Suite.instance # => #<Expectations::Suite:0x1c82c @expectations=[:expectation]>
Expectations::Suite.instance.expectations.size # => 1

最後にSuiteに格納するExpectationを実際に作成し、expectedとactualの値が等しいことを確認する。

# (略)
class Expectations::Suite
# (略)
  def expect(expected, &actual)
    expectations << Expectations::Expectation.new(expected, actual)
  end
end

# 構造体に毛が生えたようなクラス
class Expectations::Expectation
  attr_accessor :expected, :actual

  def initialize(expected, actual)
    self.expected, self.actual = expected, actual.call
  end
end

def Expectations(&block)
  Expectations::Suite.instance.instance_eval(&block)
end

Expectations do
  expect :expected do
    :expected
  end
end

Expectations::Suite.instance 
Expectations::Suite.instance.expectations.size # => 1
Expectations::Suite.instance.expectations.first.expected # => :expected
Expectations::Suite.instance.expectations.first.actual # => :expected

suiteが実行されるまでブロックの評価を遅延するなどまだやることはあるけど、open class、クラスメソッド、evalのおかげでRubyDSLを作成するのは慣れれば簡単だ。

本物のexpectationsは「sudo gem install expectations」でインストールして確かめてくれ。

訳注

とはいえ腕に覚えのある人でないと難しいと思うが…

あと、&でブロックを丸投げできることにも触れててほしいな。地味に便利だから。