2008-04-26(土) ;北海道情報大学札幌サテライト Ruby勉強会@札幌-8 DSLヒッチハイク・ ガイド The Hitchhiker’s Guide To DSL 日本Rubyの会 / Ruby札幌 島田浩二 [email protected]
May 11, 2015
2008-04-26(土) ;北海道情報大学札幌サテライトRuby勉強会@札幌-8
DSLヒッチハイク・ガイド
The Hitchhiker’s Guide To DSL
日本Rubyの会 / Ruby札幌島田浩二
Don’t Panic!パニクるな
ゴール
✓ Rubyを使ってDSLを作成する際の作戦の立て方と表現のポイントを知る
ゴール
どうぞよろしく
お願いします
DSLとは
DSLDomain Specific Language
✓ ドメインに存在する問題の解決に特化してデザインされたプログラミング言語
DSL
✓ ドメインに存在する問題の解決に特化してデザインされたプログラミング言語
DSL
✓ ドメインの専門用語を反映し、簡潔な方法でドメインの専門知識を表現するもの
DSL
✓ ドメインの専門用語を反映し、簡潔な方法でドメインの専門知識を表現するもの
DSLドメインの専門知識DSL
システム
ドメイン専門家
プログラマ
✓ DSLとそれを解釈するためのプログラミング言語との関係によって分類
DSL
二種類
内部DSL
外部DSLと
内部DSL
外部DSLと内部DSL
汎用言語透過的
内部DSL
外部DSLとRails
Ruby透過的
内部DSL
外部DSLと
外部DSLDSLパーサ
汎用言語
解釈
内部DSL
外部DSLと
XMLAnt
Java
解釈
内部DSL
外部DSLと
✓ ドメインの専門用語を反映し、簡潔な方法でドメインの専門知識を表現するもの
DSL
example
builder = Builder::XmlMarkup.new(:target => STDOUT, :indent => 2)builder.person do |b| b.name “shimada” b.phone “12345678” b.address “Sapporo, Japan”end
<person> <name>shimada</name> <phone>12345678</phone> <address>Sapporo, Japan</address></person>
http://noplans.org/product/t-shirt/
つくり方から理解を深める
DSLのつくりかた
お題
アンケート作成を支援するDSL
http://www.flickr.com/photos/koichiwb/2132785126/
アンケート作成DSL
http://www.flickr.com/photos/koichiwb/2132785126/
✓ 質問と選択肢を入力✓ HTMLに出力
DSLじゃない実現
✓ 1つのメインクラス✓ 2つのバリュー・オブジェクト✓ 1つのライブラリ
DSLじゃない実現
DSLじゃない実現
QuestionQuestionQuestionContext
Questionnaire ERB
DSLじゃない実現
QuestionQuestionQuestionContext
Questionnaire ERB
アンケート全体を表すバリューオブジェクト
質問を表すバリューオブジェクト
アンケートを出力するメインクラス
# Non-DSL Samplectx = Context.new("Ruby勉強会-8のアンケート",[])
ctx.questions << Question.new("勉強会への参加は初めてですか?", ["初めて", "常連"])
ctx.questions << Question.new("今回の勉強会はどうでしたか?", ["良かった", "まあまあ", "最低"])
ctx.questions << Question.new("次回も参加したいですか?", ["したい", "内容によって", "二度とくるか"])
Questionnaire.create_html(ctx)
# Non-DSL Samplectx = Context.new("Ruby勉強会-8のアンケート",[])
ctx.questions << Question.new("勉強会への参加は初めてですか?", ["初めて", "常連"])
ctx.questions << Question.new("今回の勉強会はどうでしたか?", ["良かった", "まあまあ", "最低"])
ctx.questions << Question.new("次回も参加したいですか?", ["したい", "内容によって", "二度とくるか"])
Questionnaire.create_html(ctx)
# Non-DSL Samplectx = Context.new("Ruby勉強会-8のアンケート",[])
ctx.questions << Question.new("勉強会への参加は初めてですか?", ["初めて", "常連"])
ctx.questions << Question.new("今回の勉強会はどうでしたか?", ["良かった", "まあまあ", "最低"])
ctx.questions << Question.new("次回も参加したいですか?", ["したい", "内容によって", "二度とくるか"])
Questionnaire.create_html(ctx)
# Non-DSL Samplectx = Context.new("Ruby勉強会-8のアンケート",[])
ctx.questions << Question.new("勉強会への参加は初めてですか?", ["初めて", "常連"])
ctx.questions << Question.new("今回の勉強会はどうでしたか?", ["良かった", "まあまあ", "最低"])
ctx.questions << Question.new("次回も参加したいですか?", ["したい", "内容によって", "二度とくるか"])
Questionnaire.create_html(ctx)
# Non-DSL Samplectx = Context.new("Ruby勉強会-8のアンケート",[])
ctx.questions << Question.new("勉強会への参加は初めてですか?", ["初めて", "常連"])
ctx.questions << Question.new("今回の勉強会はどうでしたか?", ["良かった", "まあまあ", "最低"])
ctx.questions << Question.new("次回も参加したいですか?", ["したい", "内容によって", "二度とくるか"])
Questionnaire.create_html(ctx)
class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers endend
class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions endend
class Questionnaire TEMPLATE = ... def self.create_html(context) puts ERB.new(TEMPLATE).result(binding) endend
class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers endend
class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions endend
class Questionnaire TEMPLATE = ... def self.create_html(context) puts ERB.new(TEMPLATE).result(binding) endend
class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers endend
class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions endend
class Questionnaire TEMPLATE = ... def self.create_html(context) puts ERB.new(TEMPLATE).result(binding) endend
class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers endend
class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions endend
class Questionnaire TEMPLATE = ... def self.create_html(context) puts ERB.new(TEMPLATE).result(binding) endend
class Question attr_accessor :title, :answers def initialize title, answers @title = title @answers = answers endend
class Context attr_accessor :title, :questions def initialize title, questions @title = title @questions = questions endend
class Questionnaire TEMPLATE = ... def self.create_html(context) puts ERB.new(TEMPLATE).result(binding) endend
DSLじゃない実現実際に見てみる
✓ プログラマにとっては普通のやり方✓ アンケートを作る人には使いづらい
DSLじゃない実現
✓ これをベースにアンケートを作る人でも使えるようなDSLに
DSLじゃない実現
DSLによる実現
グローバル・メソッドを使って
作戦その1
グローバル・メソッドを使って
DSLのイメージ
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 << "二度と来ない" endend
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 << "二度と来ない" endend
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 << "二度と来ない" endend
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 << "二度と来ない" endend
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 << "二度と来ない" endend
グローバル・メソッドを使って
DSLの実装
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 << qend
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 << qend
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 << qend
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 << qend
グローバル・メソッドを使って
実際に見てみる
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 << "二度と来ない" endend
表現のポイント✓ 問題領域の語彙をメソッド名に✓ ()の省略✓ ブロックの使用
✓ メソッドが増えると管理が困難✓ 小さい規模のDSL向けの戦略
グローバル・メソッドを使って
大きいDSLを作るには?
その前にちょっと寄道
表現の追求
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 << "二度と来ない" endend
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 << "二度と来ない" endend
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 << "二度と来ない" endend
名前付き引数の導入
名前付き引数の導入
DSLの実装
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 << qend
def context args @target = Context.new(args[:title], []) yield case args[:to] when :html puts ERB.new(TEMPLATE).result(binding) else p @target endend
def question text ...end
def context args @target = Context.new(args[:title], []) yield case args[:to] when :html puts ERB.new(TEMPLATE).result(binding) else p @target endend
def question text ...end
引数にキーでアクセス
表現のポイント✓ シンボルとハッシュで名前付き引数の使用✓ 意図をより明確に表現✓ 全てがオブジェクトであることを利用した表現
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 << "二度と来ない" endend
表現の追求(その2)
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 << "二度と来ない" endend
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 << "二度と来ない" endend
冗長なのでなんとかしたい
context :to => :html, :title => "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
def context args ...end
def question text q = Question.new(text, []) yield q @target.questions << qend
def context args ...end
def question text @target.questions << Question.new(text, yield)end
context :to => :html, :title => "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
だいぶアンケートらしく書けるようになった
その前にちょっと寄道ed
大きいDSLを作るには?
オブジェクトを使って
作戦その2
オブジェクトを使って
DSLのイメージ
Questionnaire.context :to => :html, :title => "R..." do |q| q.question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end q.question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end q.question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
Questionnaire.context :to => :html, :title => "R..." do |q| q.question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end q.question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end q.question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
Questionnaire.context :to => :html, :title => "R..." do |q| q.question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end q.question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end q.question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
Questionnaire.context :to => :html, :title => "R..." do |q| q.question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end q.question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end q.question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
オブジェクトを使って
DSLの実装
def Questionnaire ... def self.context args q = Question.new(args[:title], []) yield self ... end
def self. question text ... endend
Questionnaire.context :to => :html, :title => "Ruby勉強会-8の..." do |q| q.question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end q.question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end q.question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
表現のポイント
✓ クラス・メソッドを利用してDSLを実現✓ メソッドを管理しやすい✓ 若干記述が冗長に
メタプログラミングを使って
作戦その3
DSLのイメージ
メタプログラミングを使って
context :to => :html, :title => "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
オブジェクトを使って
DSLの実装
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(arg[0], yield) @target.questions << q endend
def method_missing sym, *args case sym when :context ... when :question ...
def method_missing sym, *args case sym when :context ... when :question ...
def method_missing sym, *args case sym when :context ... when :question ...
when :context @target = Context.new(args[0][:title], []) yield case args[0][:to] when :html ... end when :question q = Question.new(arg[0], yield) @target.questions << q 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(arg[0], yield) @target.questions << q endend
context :to => :html, :title => "Ruby勉強会-8のアンケート" do question "勉強会への参加は初めてですか?" do [ "初めて", "常連" ] end question "今回の勉強会はどうでしたか?" do [ "良かった", "まあまあ", "最低"] end question "次回も参加したいですか?" do [ "したい", "内容によっては", "二度と来ない"] endend
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(arg[0], yield) @target.questions << q endend
表現のポイント✓ メソッドを定義せずに動的に機能を実現✓ 利用者の負担は最低限に✓ 拡張も容易
まとめ
✓ ドメインの専門用語を反映し、簡潔な方法でドメインの専門知識を表現するもの
DSL
作戦の立て方
大きな作戦✓ グローバル・メソッド✓ オブジェクト✓ ex) クラス・メソッドの使用
✓ メタプログラミング✓ ex) method_missingの使用
小さな作戦✓ 問題領域の語彙をメソッド名に✓ 省略記法の使用✓ 名前付き引数による表現✓ ブロックによる表現
表現のポイント
表現のポイント✓ 利用者の負担は最低限に✓ 利用者の語彙で、記述は少なく
✓ 意図をより明確に✓ 何をどこに書けば良いかを明らかに
✓ 内部表現も併せて考える✓ 管理の容易性、スコープ
see also
for a good DSL hitchhiking!より良いヒッチハイクを!
ご清聴ありがとうございました
何かご質問は?