メタプログラミングRubyを読んでいます。完全自分用のメモ記事です。
第4章で特に参考になった部分を中心に書いています。
🎂 4章ブロック
ブロックの基礎
メソッドにblockを渡して、簡単に実行させることができます。
def a_method return yield if block_given? 'ブロックがありません' end
puts a_method puts a_method { 'ブロックがあります!' }
|
用語: クロージャ
『Rubyの動かないコード (初級編) ブロックとクロージャの性質 - 主に言語とシステム開発に関して』
の説明が非常に分かりやすかったので、お借りしました。まずはクロージャの説明。
- クロージャの外の(より広いスコープで定義された)変数はクロージャの中からでも参照可能
- クロージャの中で定義された変数はクロージャの外からは参照できない。
これってつまりはブロックと同じようなものということ。
- Rubyのブロックは、ブロック定義時のコンテキスト(変数とか)を保持する
- Rubyのブロック内で宣言された変数は、ブロック内でのみ参照可能な変数となる
instance_eval
instance_eval
は、渡されたブロックをレシーバのインスタンスの元で実行します。
privateメソッドや@vなどのインスタンス変数にもアクセスできます。
class MyClass def initialize @v = 1 end end
obj = MyClass.new
obj.instance_eval do puts self puts @v end
|
instance_exec
次の例はinstance_eval
だと、Cのインスタンス変数にしかアクセス出来ないが、
instance_exec
を使えばブロックに引数を渡せます。
class C def initialize @x = 1 end end
class D def twisted_method @y = 2 C.new.instance_eval { "@x : #{@x}, @y : #{@y}" } end end
puts D.new.twisted_method
class E def twisted_method @y = 2 C.new.instance_exec (@y) { |y| "@x : #{@x}, @y : #{y}" } end end
puts E.new.twisted_method
|
Procオブジェクト
inc = Proc.new { |x| x + 1 } puts inc.call(2)
inc2 = ->(x) { x - 1 } puts inc2.call(2)
|
&修飾
ブロックを引数として渡したい場合によく使うのが &修飾
。
def math(a, b) yield(a, b) end
def do_math(a, b, &block) math(a, b, &block) end
puts do_math(2, 3) { |x, y| x + y }
def my_method(greeting) "#{greeting}, #{yield}!" end
my_proc = proc { "Bill" } puts my_method("Hello", &my_proc)
|
あとで評価の例
highline
はlambdaを渡すと、それをユーザーが質問に回答した後に実行してくれる。
require 'highline'
hl = HighLine.new friends = hl.ask('友達を入力してください', lambda { |s| s.split(',') }) puts "友達一覧:#{friends.inspect}"
|
Procとlambdaの差
Procとlambdaは次のような違いがある。lambdaのほうがメソッドに挙動が近いので、
特別な事情がない限りはlambdaを使うほうが良さそう。
- Proc
- Procが定義されたスコープから戻る
- 引数が少なかったり、多すぎた場合によしなに処理をしてくれる
- lambda
- return した場合、単に lambdaから戻る
- 引数の数が異なるとArgumentErrorを出す
初めてのDSL
DSLの初歩を実践してみる。まずは redflag.rb
を作成する。
def setup(&block) @setups << block end def event(description, &block) @events << { description: description, condition: block } end
@setups = [] @events = [] load 'event.rb'
@events.each do |event| @setups.each do |setup| setup.call end puts "ALERT: #{event[:description]}" if event[:condition].call end
|
次に event.rb
を作成する。
setup do puts '空の高さを設定' @sky_height = 100 end
setup do puts '山の高さを設定' @mountains_height = 200 end
event '空が落ちてくる' do @sky_height < 300 end
event '空が近づいている' do @sky_height < @mountains_height end
event 'もうダメだ....手遅れになってしまった...' do @sky_height < 0 end
|
で実行すると次のような結果になる。
#=> 空の高さを設定
#=> 山の高さを設定
#=> ALERT: 空が落ちてくる
#=> 空の高さを設定
#=> 山の高さを設定
#=> ALERT: 空が近づいている
#=> 空の高さを設定
#=> 山の高さを設定
期待する挙動はしているが、実質的なグローバル変数があり、他のしくみに影響を与えてしまう可能性がある。
🐝 グローバル変数を排除した実装
グローバル変数を排除して、クリーンルームを使って実装したのがこちら。
lambda { setups = [] events = []
Kernel.send :define_method, :setup do |&block| setups << block end
Kernel.send :define_method, :event do |description, &block| events << { description: description, condition: block } end
Kernel.send :define_method, :each_setup do |&block| setups.each do |setup| block.call setup end end
Kernel.send :define_method, :each_event do |&block| events.each do |event| block.call event end end }.call
load '4.6.event.rb'
each_event do |event| env = Object.new each_setup do |setup| env.instance_eval &setup end
puts "ALERT: #{event[:description]}" if env.instance_eval &(event[:condition]) end
|
😎 サンプルソース
何かの役に立つこともあるかもですので、リポジトリも公開しておきます。
morizyun/meta_programming_ruby2 - GitHub
🖥 VULTRおすすめ
「VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。
最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!