ねえRuby、どこまでが君でどこからが内部DSLなの?
Rubyは内部DSL
(Domain Specific Language)に向いている言語と言われます。Rake, RSpec, Rack, Sinatraなどは内部DSL on Rubyの代表的なサンプルです。Rubyの構文のユルさとメタプログラミングが内部DSLを容易にするんですね。
しかし正直僕は、何が内部DSLで、何が内部DSLでないのかがわかっていません。人が何をさして「これは内部DSLである」と言っているのかがよくわかりません。
そんなわけで…
以下では、Userクラスの設計を通して内部DSLらしきものを作ってみます。このイテレーションに対して「ここからが内部DSLだよ」「これは内部DSLとは呼ばないよ」と、誰か僕に優しく教えてくれませんか?
Userクラスの作成
Userクラスはユーザ情報を管理するクラスです。ファイル名はuser.rbとします。
まずはユーザの登録機能を作ります。
# user.rb
class User
@@users, @@id = [], 0
attr_accessor :name, :age, :job
attr_reader :id
def initialize(name, age, job)
@@id += 1
@id, @name, @age, @job = @@id, name, age, job
@@users << self
end
def to_s
"%d:%s(%d/%s)" % [id, name, age, job]
end
end
User.new('Charlie', 12, :programmer) # => 1:Charlie(12/programmer)
User.new('Ben', 17, :teacher) # => 2:Ben(17/teacher)
生成されたUserオブジェクトは@@usersクラス変数で管理します。
次に、検索機能を付けます。id, name, age, jobの各属性で検索ができるようにします。
class User
class << self
def all
@@users
end
[:id, :name, :age, :job].each do |m|
define_method("find_by_#{m}") do |arg|
blk = ->usr { usr.send(m) == arg }
meth = [:id, :name].include?(m) ? :detect : :select
@@users.send(meth, &blk)
end
end
end
end
idとnameはユニークなものとしてdetectを、age, jobは複数の結果を返すものとしてselectを使ったfind_by
メソッドをそれぞれ定義します。
ユーザを複数登録して、検索してみます。
userlist = [
[ 'Charlie', 12, :programmer ],
[ 'Ben', 17, :teacher ],
[ 'Dick', 33, :lawyer ],
[ 'Elizabeth', 23, :doctor ],
[ 'Fernand', 27, :teacher ],
[ 'George', 33, :programmer ],
]
userlist.each { |attrs| User.new(*attrs) }
User.all # => [1:Charlie(12/programmer), 2:Ben(17/teacher), 3:Dick(33/lawyer), 4:Elizabeth(23/doctor), 5:Fernand(27/teacher), 6:George(33/programmer)]
User.find_by_id 4 # => 4:Elizabeth(23/doctor)
User.find_by_name 'Alice' # => nil
User.find_by_name 'Charlie' # => 1:Charlie(12/programmer)
User.find_by_age 33 # => [3:Dick(33/lawyer), 6:George(33/programmer)]
User.find_by_job :programmer # => [1:Charlie(12/programmer), 6:George(33/programmer)]
いいですね。
改良1(registerの導入)
さて、これでUserクラスができたので、大量にユーザを登録していきたいと思います。上のようにuserlistを作って、eachすればいいですね!
って、ちょっと躊躇しますよね。多重配列の括弧とカンマを打つのが面倒です。
もう少しマシなインタフェースを用意します。
class User
class << self
def register
yield(self)
end
alias :add :new
end
end
クラスメソッドregisterとaddを用意しました。
これらを使ったユーザ登録は、次のようになります。
User.register do |u|
u.add 'Charlie', 12, :programmer
u.add 'Ben', 17, :teacher
u.add 'Dick', 33, :lawyer
u.add 'Elizabeth', 23, :doctor
u.add 'Fernand', 27, :teacher
u.add 'George', 33, :programmer
end
User.all # => [1:Charlie(12/programmer), 2:Ben(17/teacher), 3:Dick(33/lawyer), 4:Elizabeth(23/doctor), 5:Fernand(27/teacher), 6:George(33/programmer)]
大分、入力が簡単になりました。
改良2(instance_evalの利用)
でもここまで来ると、ブロック引数でUserクラスを引き渡すのが面倒と言えば面倒です。
改善します。
class User
class << self
def register(&blk)
instance_eval(&blk)
end
alias :add :new
end
end
instance_evalを使って、registerのブロック内をUserクラスのコンテキストとして評価させます。
registerの使い方は次のように変わります。
User.register do
add 'Charlie', 12, :programmer
add 'Ben', 17, :teacher
add 'Dick', 33, :lawyer
add 'Elizabeth', 23, :doctor
add 'Fernand', 27, :teacher
add 'George', 33, :programmer
end
User.all # => [1:Charlie(12/programmer), 2:Ben(17/teacher), 3:Dick(33/lawyer), 4:Elizabeth(23/doctor), 5:Fernand(27/teacher), 6:George(33/programmer)]
更に入力が楽になりました。
改良3(userupコマンドの導入)
ここまで来たらUserクラスとユーザの登録コマンドを別ファイルにしたほうがよさそうです。
user.rbから’User.register do’ 以下を削除し、次のようなuserup
ファイルを用意します。
#!/usr/bin/env ruby
require_relative "user"
User.register do
add 'Charlie', 12, :programmer
add 'Ben', 17, :teacher
add 'Dick', 33, :lawyer
add 'Elizabeth', 23, :doctor
add 'Fernand', 27, :teacher
add 'George', 33, :programmer
end
puts User.all
userupに実行権限を付与して実行します。
% chmod +x userup
% ./userup
1:Charlie(12/programmer)
2:Ben(17/teacher)
3:Dick(33/lawyer)
4:Elizabeth(23/doctor)
5:Fernand(27/teacher)
6:George(33/programmer)
ユーザは、userup内registerでユーザを登録して、userupコマンドを実行すれば良くなりました。
改良4(configファイルの導入)
しかしながら、実行コマンド内にユーザ登録をするというのもなんか変です。
ユーザ登録は別ファイルに分離して、それを読み込むようにするのがよさそうです。user.rbのregisterメソッドを改良します。
class User
class << self
def register(cfg='userdata', &blk)
case
when blk then instance_eval(&blk)
when cfg then instance_eval ::File.read(cfg)
else raise ArgumentError
end
rescue Errno::ENOENT
abort "userdata file `#{cfg}` not found"
end
end
end
registerにブロックが渡された場合はそれを評価しますが、ブロックが無い場合はcfgファイルを読み込んで評価するようにします。cfgファイルはデフォルトで’userdata’とします。
userupコマンドは次のようにします。
#!/usr/bin/env ruby
require_relative "user"
User.register
puts User.all
そして、’userdata’ファイルを用意して、ここでaddコマンドでユーザ情報を登録していきます。
# userdata
add 'Charlie', 12, :programmer
add 'Ben', 17, :teacher
add 'Dick', 33, :lawyer
add 'Elizabeth', 23, :doctor
add 'Fernand', 27, :teacher
add 'George', 33, :programmer
準備ができたので、userup
してみます。
% ./userup
1:Charlie(12/programmer)
2:Ben(17/teacher)
3:Dick(33/lawyer)
4:Elizabeth(23/doctor)
5:Fernand(27/teacher)
6:George(33/programmer)
いいですね。
改良5(IRBの利用)
最後に仕上げとして、userupコマンドをirbを使ったインタラクティブなものにします。
#!/usr/bin/env ruby
require_relative "user"
User.register
require "irb"
IRB.start
‘userup’してみます。
% ./userup
IRB on Ruby1.9.3
>> User.all #=> [1:Charlie(12/programmer), 2:Ben(17/teacher), 3:Dick(33/lawyer), 4:Elizabeth(23/doctor), 5:Fernand(27/teacher), 6:George(33/programmer)]
>> User.find_by_name 'Fernand' #=> 5:Fernand(27/teacher)
>> User.find_by_age 33 #=> [3:Dick(33/lawyer), 6:George(33/programmer)]
>> User.find_by_job :programmer #=> [1:Charlie(12/programmer), 6:George(33/programmer)]
>> exit
いいですね。
で、最初の質問に戻ります。
どれが内部DSLなのか僕に教えてください!
Inner DSL on Ruby sample? — Gist
関連記事:
SinatraはDSLなんかじゃない、Ruby偽装を使ったマインドコントロールだ!
blog comments powered by Disqus