「DSLヒッチハイク・ガイド」で使用したコード

Ruby勉強会@札幌-8で、「RubyDSLを作成する際の作戦の立て方と表現のポイント」について発表した際に、使用したコードです。お題は「アンケート作成を支援するDSL」。

DSLじゃない実現

まず始めに、OOPで普通に実現したサンプル。ERBテンプレートはかなり適当なことになっていますが、スルーしてください。

#!/user/bin/ruby -Ku

require 'erb'

class Questionnaire
TEMPLATE = <<EOP
<b><%= context.title %></b><br/>
<% context.questions.each do |q| %>
  <p><%= q.title %></p>
  <% q.answers.each do |a| %>
    <li><%= a %></li>
  <% end %>
<% end %>
EOP

  def self.create_html(context)
    puts ERB.new(TEMPLATE).result(binding)
  end
end

# ValueObject
class Context
  attr_accessor :title, :questions
  def initialize title, questions
    @title = title
    @questions = questions
  end
end

class Question
  attr_accessor :title, :answers
  def initialize title, answers
    @title = title
    @answers = answers
  end
end

# Non-DSL sample
ctx = Context.new("Ruby勉強会-8のアンケート",[])
ctx.questions << Question.new("勉強会への参加は初めてですか?", ["初めて", "常連"])
ctx.questions << Question.new("今回の勉強会はどうでしたか?", ["良かった", "まあまあ", "最低"])
ctx.questions << Question.new("次回も参加したいですか?", ["したい", "内容によって", "二度とくるか"])
Questionnaire.create_html(ctx)

グローバル・メソッドを使って

グローバル・メソッドを使用したDSLのサンプル。

#!/user/bin/ruby -Ku

require "erb"

# ValueObject
class Context
  attr_accessor :title, :questions
  def initialize title, questions
    @title = title
    @questions = questions
  end
end

class Question
  attr_accessor :title, :answers
  def initialize title, answers
    @title = title
    @answers = answers
  end
end

TEMPLATE = <<EOP
<b><%= @target.title %></b><br/>
<% @target.questions.each do |q| %>
  <p><%= q.title %></p>
  <% q.answers.each do |a| %>
    <li><%= a %></li>
  <% end %>
<% end %>
EOP

def context text
  @target = Context.new(text, [])
  yield
  puts ERB.new(TEMPLATE).result(binding)
end

def question text
  q = Question.new(text, [])
  yield q
  @target.questions << q
end

# DSL sample
context "Ruby勉強会-8のアンケート" do
  question "勉強会への参加は初めてですか?" do |q|
    q.answers << "初めて"
    q.answers << "常連"
  end

  question "今回の勉強会はどうでしたか?" do |q|
    q.answers << "良かった"
    q.answers << "まあまあ"
    q.answers << "最低"
  end

  question "次回も参加したいですか?" do |q|
    q.answers << "したい"
    q.answers << "内容によっては"
    q.answers << "二度と来ない"
  end
end
コンテキスト部分の表現の改善

キーワード引数を導入し、コンテキスト部分の表現をちょっと改善。

...
def context args
  @target = Context.new(args[:title], [])
  yield
  case args[:to]
  when :html
    puts ERB.new(TEMPLATE).result(binding)
  else
    p @target
  end
end

def question text
  ...
end

# DSL sample
context :to => :html, :title => "Ruby勉強会-8のアンケート" do
  question "勉強会への参加は初めてですか?" do |q|
    q.answers << "初めて"
    q.answers << "常連"
  end

  question "今回の勉強会はどうでしたか?" do |q|
    q.answers << "良かった"
    q.answers << "まあまあ"
    q.answers << "最低"
  end

  question "次回も参加したいですか?" do |q|
    q.answers << "したい"
    q.answers << "内容によっては"
    q.answers << "二度と来ない"
  end
end
アンケート部分の表現の改善

つづいて、アンケート部分の表現の改善。だいぶアンケートっぽくなりました。

...
def context args
  ...
end

def question text
  @target.questions << Question.new(text, yield)
end

# DSL sample with global functions (3)
context :to => :html, :title => "Ruby勉強会-8のアンケート" do
  question "勉強会への参加は初めてですか?" do
    ["初めて", "常連"]
  end

  question "今回の勉強会はどうでしたか?" do
    ["良かった", "まあまあ", "最低"]
  end

  question "次回も参加したいですか?" do
    ["したい", "内容によって", "二度とくるか"]
  end
end

オブジェクトを使って

クラス・メソッドを使ったDSLのサンプル。

#!/user/bin/ruby -Ku

require "erb"

# ValueObject
class Context
  attr_accessor :title, :questions
  def initialize title, questions
    @title = title
    @questions = questions
  end
end

class Question
  attr_accessor :title, :answers
  def initialize title, answers
    @title = title
    @answers = answers
  end
end

class Questionnaire

TEMPLATE = <<EOP
<b><%= @target.title %></b><br/>
<% @target.questions.each do |q| %>
  <p><%= q.title %></p>
  <% q.answers.each do |a| %>
    <li><%= a %></li>
  <% end %>
<% end %>
EOP

  def self.context args
    @target = Context.new(args[:title], [])
    yield self
    case args[:to]
    when :html
      puts ERB.new(TEMPLATE).result(binding)
    else
      p @target
    end
  end

  def self.question text
    @target.questions << Question.new(text, yield)
  end
end

# DSL sample
Questionnaire.context :to => :html, :title => "Ruby勉強会-8のアンケート" do |q|
  q.question "勉強会への参加は初めてですか?" do
    ["初めて", "常連"]
  end

  q.question "今回の勉強会はどうでしたか?" do
    ["良かった", "まあまあ", "最低"]
  end

  q.question "次回も参加したいですか?" do
    ["したい", "内容によって", "二度とくるか"]
  end
end

メタプログラミングを使って

最後に、method_missingを利用したDSLのサンプル。

#!/user/bin/ruby -Ku

require 'erb'

TEMPLATE = <<EOP
<b><%= @target.title %></b><br/>
<% @target.questions.each do |q| %>
  <p><%= q.title %></p>
  <% q.answers.each do |a| %>
    <li><%= a %></li>
  <% end %>
<% end %>
EOP

# Value Object
class Context
  attr_accessor :title, :questions
  def initialize title, questions
    @title = title
    @questions = questions
  end
end

class Question
  attr_accessor :title, :answers
  def initialize title, answers
    @title = title
    @answers = answers
  end
end

def method_missing sym, *args
  case sym
  when :context
    @target = Context.new(args[0][:title], [])
    yield
    case args[0][:to]
    when :html
      puts ERB.new(TEMPLATE).result(binding)
    else
      p @target
    end
  when :question
    q = Question.new(args[0], yield)
    @target.questions << q
  end
end

#DSL sample
context :to => :html, :title => "Ruby勉強会-8のアンケート" do
  question "勉強会への参加は初めてですか?" do
    ["初めて", "常連"]
  end

  question "今回の勉強会はどうでしたか?" do
    ["良かった", "まあまあ", "最低"]
  end

  question "次回も参加したいですか?" do
    ["したい", "内容によって", "二度とくるか"]
  end
end