LoginSignup
3
0

More than 1 year has passed since last update.

Ruby の DSL はどのように実装されているか

Last updated at Posted at 2023-04-21

DSL(domain-specific language)が何なのかを正確に説明しようとすると難しいが、特定のタスクをうまく記述することを目的としたプログラミング言語だとされている。今回は Ruby 界隈で(主に Ruby on Rails 界隈か?)特に有名なテストフレームワーク RSpec を例にして、DSL がどう実装されているかを紹介する。

RSpec での例

RSpec はこんな感じにテストを記述していくことができるテストフレームワークだ。

it do
  val = 'example'
  expect(val).to eq 'example'
end

なんか Ruby っぽくない感じもする。というか実際、ある意味で特殊な書き方ができるようにわざわざ設計したのが DSL なわけなので、そう見えるのは当然でもある。

DSL の実装例

さて、本記事ではこういった DSL がどうやって実装されているかを理解するために、実際に RSpec のような DSL を実装していく。DSL は、まず「こう書きたい」というものを前提にし、そこから「そう書けるようにするためにはどう作れば良いか?」を逆算してコードを書いていくと良い。今回は RSpec を参考にして、以下のように書ける DSL を自分で作ってみよう。

DSLExample.it do
  val = 'example'
  expect(val).to eq 'example'
end

ここでは簡単に、expect に渡したテスト結果 val が、 eq の期待値と一致すれば OK! を、違えば NG! を表示するということにする。

肝となるのはこの部分だ。

expect(val).to eq 'example'

ふつうの Ruby コードっぽくない見た目だが、 RSpec は(そして RSpec を参考にした本例の DSL は) internal DSL であり、これ自身 Ruby のコードである。Ruby にとってこのコードがどう解釈されるか、分かりやすくするために括弧を付与するとこうなる。

expect(val).to(eq('example'))

ここから「こう書けるように」逆算して実装を行っていく。まずこの文はいくつかの構成要素に分けられる。

  • expect:この実行文脈でのメソッド
  • val:前行で初期化されたローカル変数
  • toexpect(val) が返したオブジェクトのメソッド
  • eq:この実行文脈でのメソッド
  • 'example':文字列リテラル

このうち val'example' は特に難しいことはない。 to は今すぐ考えるのは難しそうだ。最初に手を付けるのは expecteq である。これらはこの文脈において呼び出し可能なメソッドでなければならない。なので、まずこの文脈自体を作っていこう。

ではこの文脈は何だったかというと、DSLExample クラスの it メソッドに渡されたブロックである。

DSLExample.it do
  ...
end

素直にそれを実装しよう。

class DSLExample
  class << self
    def it(&block)
      # TODO: implement
    end
  end
end

さて、ここで引数として受け取ったブロックは、 expecteq メソッドが呼び出せる文脈でなければならない。そうするためには、それらメソッドを持つオブジェクトの instance_exec を呼び出すのが常套手段だ。

class ItContext
  def expect(val)
    # TODO: implement
  end

  def eq(val)
    # TODO: implement
  end
end

class DSLExample
  class << self
    def it(&block)
      ItContext.new.instance_exec(&block)
    end
  end
end

これで it に渡したブロック内で expecteq が呼び出せるようになった。

次は expect を考える。このメソッドはなんらかのテスト結果を受け取り、後々その値を期待値と比較するために保持しておく必要がある。また、このメソッドが返すオブジェクトは to メソッドを持っていなければならない。

class Expect
  def initialize(val)
    @val = val
  end

  def to(matcher)
    # TODO: implement
  end
end

class ItContext
  def expect(val)
    Expect.new(val)
  end
end

eq はどうだろう?これはテストの期待値を保持し、比較方法(ここでは eq すなわち == とする)を知っているオブジェクトを返すべきだ。比較方法を尋ねるには validate というメソッドを使うことにしよう。

class EqMatcher
  def initialize(val)
    @val = val
  end

  def validate(other)
    if @val == other
      puts "OK!"
    else
      puts "NG!"
    end
  end
end

class ItContext
  def eq(val)
    EqMatcher.new(val)
  end
end

もう少しだ。eq が返した EqMatcher オブジェクトはどこへ渡されるかというと、 to メソッドである。最後の実装をしよう。 to メソッドは渡されたオブジェクトにテスト結果を渡して、 OK なのか NG なのかを判断してもらえばよい。to メソッドを持っている Expect オブジェクトはテスト結果も持っているんだったよね?

class Expect
  def initialize(val)
    @val = val
  end

  def to(matcher)
    matcher.validate(@val)
  end
end

これで完成だ。

使ってみる

では使ってみよう。

DSLExample.it do
  val = 'example'
  expect(val).to eq 'example'
end

DSLExample.it do
  val = 'example'
  expect(val).to eq "foobar"
end
OK!
NG!

できた!

拡張

ここまでできれば、 expect に値ではなくブロックを渡せるようにしたり、一致以外の条件でテストを行う新たな Matcher を追加したりということも、どうすればいいかなんとなく想像がつくのではないだろうか。この例を元に実装してみるのも面白いかもしれない。

完成コード全体

class DSLExample
  class << self
    def it(&block)
      ItContext.new.instance_exec(&block)
    end
  end
end

class ItContext
  def expect(val)
    Expect.new(val)
  end

  def eq(val)
    EqMatcher.new(val)
  end
end

class Expect
  def initialize(val)
    @val = val
  end

  def to(matcher)
    matcher.validate(@val)
  end
end

class EqMatcher
  def initialize(val)
    @val = val
  end

  def validate(other)
    if @val == other
      puts "OK!"
    else
      puts "NG!"
    end
  end
end



DSLExample.it do
  val = 'example'
  expect(val).to eq 'example'
end

DSLExample.it do
  val = 'example'
  expect(val).to eq "foobar"
end

まとめ

RSpec を参考に、簡易的な DSL の実装例を紹介した。RSpec に限らず、Ruby で実装された DSL は instance_exec でその言語の文脈に入り、メソッドとして言語機能を提供し、それらメソッドはそこで必要な情報や、そこから続けて提供したい言語機能を提供するオブジェクトを返す、といった形で実装されていることが殆どだ。自分で DSL 自体を作ることはないとしても、こういったことを把握しておくと、既存 DSL の拡張を作りたくなったり、あるいはトラブルシューティングをしなければならなくなったりといったときに役に立つ。見た目に騙されがちだが(ある意味で騙したいのが DSL というものでもある)、結局は Ruby コードそのものなので、注意深く読めばどうなっているのか分かるはずだ。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0