- 更新日: 2015年2月13日
- Ruby
RubyでDSL作成入門、メタプログラミングRubyを読んで
メタプログラミングRubyを何回も読み返していまして、現在5回目を読み終わったところです。さすがに5回も読みますと、Rubyのメタプログラミングな側面の理解がかなり進みました。ということで Ruby での DSL(domain specific language)作成の入門として、簡単な DSL を書いてそれを実行できるように実装してみました。メタプログラミングRubyの第3章あたりの内容です。
DSL の仕様作成
ディレクトリの構造は以下、DSL ファイル自体は tests/bmi_profile.rb に書きました。judge_profile.rb が DSL の処理を行う実装ファイルとなります。
1 2 3 4 5 |
judge_profile.rb tests/ |--bmi_profile.rb |
以下のような DSL(内部DSL)を書けるようにします。setup メソッドで、プロフィール用の身長(@height)・体重(@weight)・BMI値(@bmi) を、各々 profile メソッドの実行前にセットアップします。全て setup された値を元に、それぞれ profile メソッド呼び出しのブロックを評価して、ブロックが true を返したものだけを出力するという仕様。
tests/bmi_profile.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
profile "height is low" do @height < 160 end profile "height is normal" do 160 <= @height && @height < 180 end profile "height is high" do 180 <= @height end profile "weight is light" do @weight < 50 end profile "weight is normal" do 50 <= @weight && @weight < 70 end profile "weight is heavy" do 70 <= @weight end profile "bmi is low" do @bmi < 18.5 end profile "bmi is normal" do 18.5 <= @bmi && @bmi < 25 end profile "bmi is high" do 25 <= @bmi end setup do puts "--- set height and weight ---" @height = 168 @weight = 58 end setup do puts "--- set bmi ---" @bmi = @weight.to_f / (@height.to_f/100)**2 end |
DSL を読み込んで実行時の出力
上記の DSL ファイルを読み込んで実行させますと、次の出力となるように judge_profile.rb を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ ./judge_profile.rb --- set height and weight --- --- set bmi --- --- set height and weight --- --- set bmi --- height is normal --- set height and weight --- --- set bmi --- --- set height and weight --- --- set bmi --- --- set height and weight --- --- set bmi --- weight is normal --- set height and weight --- --- set bmi --- --- set height and weight --- --- set bmi --- --- set height and weight --- --- set bmi --- bmi is normal --- set height and weight --- --- set bmi --- |
“— set height and weight —“, “— set bmi —” が表示され、各々 profile メソッド呼び出しの前に、毎回全てセットアップされています。そして、profile メソッド呼び出しのブロックの評価が true のものだけ出力。上記 DSL の例ですと、セットアップした値が @height(168)、@weight(58)、@bmi(20.5) ですので、各々 normal を出力しています。
DSL を読み込んで実行させるための実装
では、judge_profile.rb の実装です。DSL ファイル(tests/*profile.rb)を読み込んで、結果を出力させるための実装となります。
judge_profile.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#!/usr/bin/env ruby lambda { setups = [] profiles = {} Kernel.module_eval do define_method :profile do |entry, &block| profiles[entry] = block end define_method :setup do |&block| setups << block end define_method :each_profile do |&block| profiles.each_pair do |entry, profile| block.call entry, profile end end define_method :each_setup do |&block| setups.each do |setup| block.call setup end end end class CleanRoom end }.call Dir.glob('./tests/*profile.rb').each do |file| load file each_profile do |entry, profile| env = CleanRoom.new each_setup do |setup| env.instance_eval(&setup) end puts entry if env.instance_eval(&profile) end end |
あとはこの judge_profile.rb のパーミッションを755にして実行すると、DSL のファイル(tests/*profile.rb)を処理できます。
使ってる技術を少々解説
詳しくは、メタプログラミングRubyを読んでくさい!と言いたいところですけど、少々説明。
まずは、Dir.glob で ./tests/ ディレクトリ内の DSL ファイルを全て読み込んでいます(load file)。load file 時に、judge_profile.rb の define_method で定義した、setup, profile により setups 配列と profiles ハッシュが設定される。
lambda
lambda 内にコードを閉じ込めて call することで、グローバル領域の不要な汚染を防ぐ。
Module#module_eval
DSL の profile, setup メソッドは、レシーバ指定なしで呼べるように Kernel モジュールのメソッドとして定義する。また、lambda 内の最初で初期化した setups 配列、profiles ハッシュにアクセスできるように、Module#module_eval を使う。”module Kernel ~ end” によるモンキーパッチだと、スコープが変わりそれらにアクセスできなくなる。(フラットスコープ/クロージャ)
Module#define_method
Module#define_method は動的にメソッドを定義する手段、それと同時にフラットスコープにより setups, profile にアクセスできる。”def profile ~ end” の書き方だとスコープが変わり、それらにアクセスできなくなる。
&block と block.call
ブロックをメソッドの定義時に引数として受けとるには、&block という風に書きます。block 自体は Proc オブジェクトなので、実行するときには block.call で呼び出す。”block.call entry, profile” とすることで、ブロック引数を渡せます。
class CleanRoom
setup, profile のブロックを評価するためのクリーンルーム用クラス。このオブジェクトで instance_eval することで、このオブジェクトのインスタンス変数として、DSL ファイル内の @height, @weight, @bmi が setup され、profile のブロックが評価される。
BasicObject#instance_eval
レシーバのコンテキストで、ブロックを評価します。
以上です。このくらいの内部 DSL であれば、このように簡単に実装することができます。テストフレームワークの仕組みをある程度理解できました。メタプログラミングを学ぶと、さらなる Ruby の柔軟性と動的なパワーに驚かされますね。
ちなみに、繰り返しメタプログラミングRubyを読んでいるのは、メタプログラミングRubyがそれに値する本であるのと、7回読み勉強法を試してみているのが理由です。初回読んだ時は読了に何日もかかったのですが、5回目は2時間程度で読み終えられたので、どんどん楽になると同時に理解も深くなって良い感じです。
教科書を7回読むだけで、断然トップになれた!(前編):PRESIDENT Online – プレジデント
- Ruby の関連記事
- Gemの作り方(Ruby Gem)
- ローカル開発中のgemをGemfileに書いてインストール
- 熊本地震の余震が夜に多いのは本当か?Rubyプログラムで検証してみた
- El Capitanでgemのnative extensionビルド失敗に対応
- Rubyで親クラスから子クラスの定数を参照
- MacabをRubyで使う
- rbenv/ruby-buildでRuby最新バージョンをインストール
- Rubyでクラスインスタンス変数にインスタンスメソッドからアクセス
- 距離1kmあたりの緯度・経度の度数を計算(日本・北緯35度)
- Google Maps Geocoding APIで住所から緯度・経度を取得するRubyコード
Leave Your Message!