メタプログラミングRuby第2版 / 第4章ブロック[勉強メモ]

メタプログラミング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 #=> <MyClass:0x007ff9f89dcde0>
puts @v #=> 1
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 #=> @x : 1, @y :
class E
def twisted_method
@y = 2
C.new.instance_exec (@y) { |y| "@x : #{@x}, @y : #{y}" }
end
end
puts E.new.twisted_method #=> @x : 1, @y : 2

Procオブジェクト

inc = Proc.new { |x| x + 1 }
puts inc.call(2) # => 3
# lambdaの別記法
inc2 = ->(x) { x - 1 }
puts inc2.call(2) #=> 1

&修飾

ブロックを引数として渡したい場合によく使うのが &修飾

def math(a, b)
yield(a, b)
end
def do_math(a, b, &block)
math(a, b, &block)
end
# block を Proc(オブジェクト)に変換して渡す
puts do_math(2, 3) { |x, y| x + y } #=> 5
# Procをブロックに戻す
def my_method(greeting)
"#{greeting}, #{yield}!"
end
my_proc = proc { "Bill" }
puts my_method("Hello", &my_proc) #=> "Hello Bill!"

あとで評価の例

highline はlambdaを渡すと、それをユーザーが質問に回答した後に実行してくれる。

require 'highline'
hl = HighLine.new
friends = hl.ask('友達を入力してください', lambda { |s| s.split(',') })
puts "友達一覧:#{friends.inspect}"
# => 友達を入力してください
# <= hoge,fuga
# => 友達一覧:["hoge", "fuga"]

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
# 空の高さを設定
# 山の高さを設定
# ALERT: 空が落ちてくる
# 空の高さを設定
# 山の高さを設定
# ALERT: 空が近づいている
# 空の高さを設定
# 山の高さを設定

🗻 サンプルソース

何かの役に立つこともあるかもですので、リポジトリも公開しておきます。

morizyun/meta_programming_ruby2 - GitHub

📚 おすすめの書籍

💩 欲しいものリスト公開しました