みなさん、こんにちは!エンジニアの小澤です。
9/8~9/10の三日間にわたり京都で開催されたRubyKaigi 2016のレポートの後半をお送りします。素晴らしいトークばかりでしたが、私の中で特に興味深かったセッションを取り上げたいと思います。
How DSL Works On Ruby
スピーカーである柴田氏は、Ruby Prizeに何度も選ばれたRubyコミッタです。
Rails girl Tokyoのコーチを務めるなどRubyの啓蒙活動へ積極的に取り組まれています。
rakeの成り立ち
RakeはRubyで実装されたビルドツールであり、Rubyによる言語内DSLという実装アプローチをしています。元々はMakeのような機能をrubyで実現するためのビルドツールとして作られました。
ビルドツールを利用するには、ビルド用のファイルを用意して、実行指示を記述します。
RakeのビルドファイルはRakefileであり、これはmakeのMakefile、antのbuild.xmlに該当します。
Rakeの特徴は複雑なビルドを柔軟に書けることで、タスクと依存関係をRubyの文法で定義できます。 その理由は内部DSLという仕組みにあります。
外部DSLは汎用プログラミング言語とは全く異なる文法で記述するのに対して、内部DSLでは、Rubyの文法に則って記述します。
そのため、ビルドにRubyの文法をそのまま使うことができ、高い自由度を実現しています。
DSLの概要
DSL(Domain-Specific Language)とは、Java,C#などの汎用言語とは異なり、ある特定の種類の問題に特化したコンピュータ言語です。Ruby on Raisの登場以降、メタプログラミング(マクロ、コード自動生成など)が普及してきました。 DSLはメタプログラミングで使用され、「あるプログラムを生成するプログラム」を書くことに使用されます。 メタプログラミングの詳細については割愛しますが、ご興味のある方は以下のリンクをご参照ください。
Rubyは内部DSLをビルドするための機能をたくさん持っており、メタプログラミングのテクニックを使用します。
Rubyはそれ自体が可読性が高いため、別のドメイン言語をパースする外部DSLより、 Rubyで書いたものを別のドメイン言語として解釈する内部DSLのほうがマッチしていると考えられます
パターン:Class method
Userクラスでhas_many :foo を記述したい場合は特異クラスのメソッドにするだけで良い。
class User
has_many :foo
end
class User
def self.has_many(foo)
puts foo
end
end
パターン:Module and Class ancestors
has_manyを様々な場所で使用する場合には、moduleでhas_manyを定義し、ARBaseでextendして継承すれば良い。
module DSL
def has_many(bar = nil)
puts bar
end
end
class ARBase
extend DSL
end
Rubyのモジュールとクラスを使って簡単なDSLを作れる
class < ARBase
has_may :blogs
end
パターン:Method define
class < ARBase
has_may :blogs
blogs_foo
end
module DSL
def has_many(bar = nil)
self.class.module_eval <<-CODE, __FILE__, ___LINE_
def #{bar}_foo
puts :foo
end
CODE
end
end
(snip)
パターン:Inplicit code block
Foo.configure do |c|
c.bar = :buzz
end
module Foo
def self.configure
yield self
end
class << self
attr_accessor :bar
end
end
パターン:Method define
class < ARBase
has_may :blogs
blogs_foo
end
module DSL
def has_many(bar = nil)
self.class.module_eval <<-CODE, __FILE__, ___LINE_
def #{bar}_foo
puts :foo
end
CODE
end
end
(snip)
パターン:Decractive Setter
Foo.configure do
bar :buzz
end
module Foo
def self.configure(&block)
instance_eval(&block)
end
class << self
def bar(v = nil)
v ? @bar = v : @bar
end
end
end
パターン:instance_eval
gemfile = <
rakeコマンドの紹介
Rake::FileListは、ファイル一覧を操作するときに便利なクラスです。 複数指定した後で一部除外して取り出すことができます。
file_list = FileList.new('lib/**/*/.rb', 'test/test*.rb7)
FileList['a.c', 'b.c'].exclude("a.c") => ['b.c']
Rake::TestTaskは、MinitestやTest::Unitで実行します。
歴史的経緯でライブラリの外に残っていますが、rdoc、bundle等にはそれぞれのgemにあります。
require 'rake/testrask'
Rake:TestaTask.new(:test) do |t|
t.libs << "test"
t.verbose = true
t.test_files = FileList['test/**/test_*.rb']
end
Gemの中のDSL
RakeRake.applicationはシングルトンとして扱います。 rake_module.rbでは、RakeインスタンスのためのRake.applicationが定義されています。 Rake.applicationは、Rake::Applicationインスタンスを返します。 rakeコマンドではrake:applicationインスタンスのrunメソッドが呼ばれます。
def run
standard_exception_handling do
init
load_rakefile
top_level
end
end
initメソッドは、呼び出すタスクを検出し、load_rakefileは、デフォルトのrakeファイルを読みます。rubyのloadメソッドを使って、load_rakefileは簡単にRakefileをロードすることができます。
top_levelは、rakeのスレッドプ-ル下にあり、コマンドラインから与えられたタスクを呼び出します。
オプションで-Tを付けた場合は、rakefileタスクの一覧を表示します。
オプションで-Pを付けた場合は、ターゲットタスクに依存している一覧を表示します。
RakeファイルのDSLを解釈するために、Rake::DSLが定義されています。
module Rake
module DSL
(snip)
def tesk(*args, &block) # :doc:
Rake::Task.define_task(*args, &block)
end
end
end
self.extend Rake::DSL
外部向けにタスク一覧を取得したり、消したりするのはrake:taskです。主にmigration等の用途に使用されます。
rakeでは、ハッシュの中にrake:taskインスタンスがキーとして入り、実行されます。
実行タスクのdo endの中に入っているオブジェクトを順番に実行します。
その他gemのDSL
- capstranoは自動デプロイスクリプトをビルドするフレームワークであり、rakeを継承しています。
- Thorは効率的なコマンドラインユーティリティの自動ドキュメント化ビルドツールです。CLIのために継承されたDSLパターンを提供します。
- bundlerはGemパッケージの管理を行います。Bundle::CLIはThorクラスを継承しています。bundlerは、GemfileのDSLを提供します。
Rakeの歴史と今後について
最後にRakeの今後についてのお話しがありました。
rake10からメンテナンスが開始されたのですが、rake11ではlastcommentメソッドを消したら、 そのメソッドを使用していたrspecでエラーが発生して急遽戻すなどしたり、bundlerが使えるように書き直したりと 様々なトラブルがあったそうです。普段何気なくgemを使用していますが、コミッターの方達のお陰で日々楽しく開発できていることに 感謝です。
rake12の方針
- rake12では、ruby2.2またはrails5以上サポートしか行わない
- rakeの複雑な仕組みを簡素化して、可読性、実行速度を考慮したコードに書き換える
- defaultタスクの記述を追加
最後に柴田氏から、DSLについて理解を深めるには、以下の書籍を読むのが良いとのご紹介がありました。
閉会式
RubyKaigi2016は連日大盛況で非常に盛り上がっていました。 私はここまで大きなカンファレンスに参加したことがなかったので、第一線で活躍するコミッター方や世界中からの熱心な参加者を 目の当たりにすることができ、とても良い刺激がいただけました。 またセッションに参加する中で、まずは自身もgemを作って公開したいと思いました。
では、皆さんまた来年RubyKaigi2017でお会いしましょう!
[参考資料]
How DSL works on Ruby
今そこにある“DSL”
