酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

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

メタプログラミングRubyを読んでいます。完全自分用のメモ記事です。 第4章で特に参考になった部分を中心に書いています。


4章 ブロック

ブロックの基礎

メソッドに block を渡して、簡単に実行させることができます。

1
2
3
4
5
6
7
def a_method
  return yield if block_given?
  'ブロックがありません'
end

puts a_method #=> ブロックがありません
puts a_method { 'ブロックがあります!' } #=> ブロックがあります!

用語: クロージャー

Rubyの動かないコード (初級編) ブロックとクロージャの性質 - 主に言語とシステム開発に関して』 の説明が非常に分かりやすかったので、お借りしました。まずはクロージャの説明。

- クロージャの外の(より広いスコープで定義された)変数はクロージャの中からでも参照可能
- クロージャの中で定義された変数はクロージャの外からは参照できない。

これってつまりはブロックと同じようなものということ。

- Rubyのブロックは、ブロック定義時のコンテキスト(変数とか)を保持する
- Rubyのブロック内で宣言された変数は、ブロック内でのみ参照可能な変数となる

instance_eval

instance_evalは、渡されたブロックをレシーバのインスタンスの元で実行します。 private メソッドや@vなどのインスタンス変数にもアクセスできます。

1
2
3
4
5
6
7
8
9
10
11
12
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 を使えば ブロックに引数を渡せます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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オブジェクト

1
2
3
4
5
6
inc = Proc.new { |x| x + 1 }
puts inc.call(2) # => 3

# lambdaの別記法
inc2 = ->(x) { x - 1 }
puts inc2.call(2) #=> 1

&修飾

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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を渡すと、それをユーザーが質問に回答した後に実行してくれる。

1
2
3
4
5
6
7
8
9
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 を作成する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 を作成する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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: 空が近づいている
#=> 空の高さを設定
#=> 山の高さを設定

期待する挙動はしているが、実質的なグローバル変数があり、他の仕組みに影響を与えてしまう可能性がある。

グローバル変数を排除した実装

グローバル変数を排除して、クリーンルームを使って実装したのがこちら。

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
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


おすすめの書籍