近年、特定の領域に存在する問題の解決に特化してデザインされたコンピュータ言語「DSL」が注目を集めている。本稿では、前回に引き続いて、RubyとDSLの関係について解説していく。
この連載のバックナンバーは以下の通りです。併せてお楽しみください。
連載第1回:Instant Railsで始めるWindows環境のRails
連載第2回:Rails専用IDE「RadRails」でRailsをもっともっと快適に
連載第3回:話題騒然! 「言語内DSL」の概念とRake【前編】
前回紹介したとおり、言語内DSLはホスト言語そのものを利用してDSLを構築します。したがって、言語内DSLではホスト言語の機能をフル活用できます。Rubyで構築された言語内DSLであれば、Rubyの制御構造の利用はもちろん、独自にクラスやメソッドを定義できますし、サードパーティーのRubyライブラリを利用することも可能です。
Rakefileは、実行時にそのままRubyのコードとして評価されます。ここで、前回紹介したhelloプログラム用のRakefile(リスト1)にRubyのメソッドを定義してみましょう。参考までに(リスト1)を再度示しておきます。
1 require 'rake/clean'
2 CLEAN.include %w(hello *.o)
3
4 task :default => ['hello']
5
6 file 'main.o' => ['main.c', 'greet.h'] do
7 sh 'cc -c -o main.o main.c'
8 end
9
10 file 'greet.o' => ['greet.c'] do
11 sh 'cc -c -o greet.o greet.c'
12 end
13
14 file 'hello' => ['main.o', 'greet.o'] do
15 sh 'cc -o hello main.o greet.o'
16 end
例えば、リスト1の6〜12行目をリスト2のように修正すると、コンパイル部分をメソッドとしてくくり出せます。これを見れば、Rakeのファイルタスクのような成果物間の依存関係の宣言と、コンパイル方法のような具体的な手続きの記述とを、Rakefileに対して違和感なくまとめて簡潔に記述できていることが分かるはずです。ビルド時に必要な処理やその組み合わせが複雑になった場合、Rubyの機能をフル活用できることがRakeの大きなアドバンテージとなります。
6 def compile(dest, *src)
7 sh "cc -c -o #{dest} #{src.join(' ')}"
8 end
9
10 file 'main.o' => ['main.c', 'greet.h'] do
11 compile 'main.o', 'main.c'
12 end
13
14 file 'greet.o' => ['greet.c'] do
15 compile 'greet.o', 'greet.c'
16 end
Rubyで実装された言語内DSLの文法の多くは、通常のメソッド呼び出しです。通常のメソッド呼び出しを、あたかもDSL用の新たな文法であるかのように読み書きできるというのがポイントです。このことは、DSLツールの実装者の負担も軽減します。
言語内DSLの文法としてRubyを利用しやすい理由には、次のようなRubyの特徴が挙げられます。
以下、順番に紹介します。
Rubyでは、多くの場合*、文末のセミコロンをはじめとして、メソッド呼び出しの括弧やハッシュのリテラルの記述などを省略できます。省略記述をうまく活用することで、Ruby上では単なるメソッド呼び出しにすぎない記述を、あたかも別の言語による宣言のように見せることができます。
Rakeのタスク記述を、
task :test => [:compile] do
do_task1
do_task2
end
のように書けるのも、Rubyの省略表記のおかげです。この記述は、
task({:test => [:compile]}) {
do_task1();
do_task2();
}
と等価ですが、このように書いてしまうとDSLの文法というよりは、いかにもAPIメソッドの呼び出しのように読めてしまいます。
Rubyでは、メソッド呼び出し時に「do……end」で括ったブロックを渡すことができます。ブロックには特定のコンテキストにおける一連の処理を記述します。
これを利用して、Rakeではタスクの定義にブロックを使います。
task :test do
ruby "test/unittest.rb"
end
タスク定義にブロックを渡すことと、省略表記を活用することで、「task」がメソッド呼び出しというよりはタスク記述用の文法であるかのように見せています。
Rubyでは、あらゆるクラスが拡張に対して開かれています。コアライブラリのクラスであっても例外ではありません。これはRailsでの例になりますが、ActiveSupportというユーティリティはコアライブラリのNumericクラスを拡張しています。この拡張によって、Railsでは「現在時刻から20時間前」を「20.hours.ago」と記述できます。
>> 20.hours.ago
=> Fri Jan 13 11:13:13 JST 2006
ActiveSupportのRubyコアライブラリ拡張は容赦がなく、true、false、nilといった表現をするクラスも拡張の対象としています。
ここまで読んだ人はすでに気づいているかもしれませんが、現在、最も成功しているRubyによる言語内DSLツールはRailsです。Railsは「Webアプリケーション」という問題を解決するためにRubyを利用しています。もちろん言語内DSLなので、Rails内ではRubyをフル活用できます。「Ruby on Rails」という名前は、「Railsという名のDSLのレールにRubyを乗せて行く」と解釈できるかもしれません。
このようなDSLライクな記述を活用したものはRailsの周辺ツール*でも充実しており、その代表的なものはmigration*とSwitchTowerです。前者はデータベースのスキーマのDDL*を、後者はRailsアプリケーションのデプロイ方法を、それぞれRubyを利用した言語内DSLとして記述します。
ここで、migrationでの簡単なテーブル定義を紹介しましょう。リスト3では、CreateUserTable#upというメソッドと、CreateUserTable#downというメソッドで、テーブルの作成と削除を定義しています(それぞれのSQL記述はリスト4)。これだけでは単にDDL記述を抽象化しているだけですが、migrationではデータベースのスキーマのバージョン管理も実現可能です。
class CreateUserTable < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.column :name, :string, {:limit => 100, :null => false}
t.column :hashed_password, :char, {:limit => 10, :null => false
end
end
def self.down
drop_table :users
end
end
4A テーブルの作成
CREATE TABLE users (
name VARCHAR(100) NOT NULL
,hashed_password CHAR(10) NOT NULL
);
4B テーブルの削除
DROP TABLE users;
本稿で紹介してきたツールは、すべてRakeと組み合わせて利用できます。Railsはrailsコマンドで生成したアプリケーションの開発で利用するRakefileを、拡張可能な形式で提供しています。Rake本体の動作とは若干異なるRails流の規約ですが、それに従うことで簡単に拡張できるわけです。
ユーザー定義のRakeタスクをRailsの提供するRakefileと統合して利用するには、ユーザー定義のRakeタスクを記述したファイル(*.rake)をRailsアプリケーションの開発ディレクトリ以下にあるlib/tasksディレクトリに配置すれば完了です。これによって、ユーザー定義のRakeタスクとRailsの提供するRakeタスクとを同じように利用できます。
SwitchTowerは、この仕組みを利用しています。switchtowerコマンドでは、独自のRakeタスクが生成されます。このタスクはRailsアプリケーションの開発で利用するRakefileに統合されるので、違和感なく利用することが可能です。
以上、2回にわたってDSLの概念とRakeについて解説してきました。「Rubyを言語内DSL構築用に利用して、簡潔な文法で宣言的な記述と手続き的な記述をうまく取り合わせ、それをRakeと組み合わせる」というケースは、これからもますます増えていくでしょう。
コンテキストによっては省略できない場合もある。
Railsと直接関係ないところではRubiniumがある。
http://wiki.rubyonrails.com/rails/pages/UnderstandingMigrations
Data Definition Languageの略で、リレーショナルデータベースのテーブルを制御する言語。
本記事は、オープンソースマガジン2006年3月号「Ruby on Rails 1.0の世界」を再構成したものです。
Copyright © ITmedia, Inc. All Rights Reserved.