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

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

# RSpec での例

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

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

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

# DSL の実装例

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

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

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

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

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

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

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

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

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

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

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

```ruby
DSLExample.it do
  ...
end
```

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

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

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

```ruby
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` に渡したブロック内で `expect` と `eq` が呼び出せるようになった。

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

```ruby
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` というメソッドを使うことにしよう。

```ruby
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` オブジェクトはテスト結果も持っているんだったよね？

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

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

これで完成だ。

## 使ってみる

では使ってみよう。

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

DSLExample.it do
  val = 'example'
  expect(val).to eq "foobar"
end
```
```console
OK!
NG!
```
できた！

## 拡張

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

## 完成コード全体

```ruby
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 コードそのものなので、注意深く読めばどうなっているのか分かるはずだ。