ブロックとProcの世界

Rubyの世界はブロックとProcで溢れている。だのになぜ、自分はそれに精通できないのか?Symbol#to_procとか、関数型Rubyとか、そんな発想は自分には到底できそうもない。そればかりか、自分の頭の中はRuby1.8で止まっている。その状態では、Ruby1.9以降に追加された新たな記法が、謎の記号に見えてしまう...。

発想はできないけど、そうゆうコードを読んで感動できる読解力は持ち続けたい。調べてみた。

環境

irb(main):001:0> RUBY_VERSION
=> "2.0.0"
$ ruby -v
ruby 2.0.0p451 (2014-02-24 revision 45167) [universal.x86_64-darwin13]

ブロックとは何か?

do ... end、あるいは{ ... }で囲まれた一連のコードのこと。

例1:

['apple', 'orange', 'melon'].each do |word| puts word.capitalize end
# 出力結果
Apple
Orange
Melon
=> ["apple", "orange", "melon"]

例2:

(1..10).inject{ |a, b| a + b }
# 出力結果
=> 55
  • ブロックは、メソッドの最後の引数としてのみ存在できる。
    • よって、引数に指定できるブロックは1つだけ。複数のブロックは指定できない。
    • 但し、後述する方法で、複数のProcオブジェクトに変換して渡すことは可能だ。
irb(main):078:0> 1.times { puts "hello, world!" }
hello, world!
=> 1
  • ブロックだけではエラーになる。
irb(main):086:0> do puts "hello, world!" end
SyntaxError: (irb):86: syntax error, unexpected keyword_do_block
do puts "hello, world!" end
  ^
...中略...

irb(main):080:0> { puts "hello, world!" }
SyntaxError: (irb):80: syntax error, unexpected tSTRING_BEG, expecting keyword_do or '{' or '('
{ puts "hello, world!" }
        ^
...中略...

ブロックの変数

ブロックパラメータ
  • ブロックパラメータとは、ブロックの先頭で| ... |で囲って設定された仮引数のこと。
    • ブロックが実行される時に、メソッドから値を受け取る役割がある。
  • 基本的に、メソッドの仮引数と同じルールである。
    • 可変長の配列を受け取ったり、
irb(main):078:0> (1..10).inject{|*args| p args; args.shift + args.shift}
[1, 2]
[3, 3]
[6, 4]
[10, 5]
[15, 6]
[21, 7]
[28, 8]
[36, 9]
[45, 10]
=> 55
    • デフォルト値を指定したり。
irb(main):082:0> (1..10).inject{|a, b, c=100| p c; a + b}
100
100
100
100
100
100
100
100
100
=> 55
ブロックパラメータの変数のスコープ
  • ブロックパラメータは、そのブロック内でのみ有効な変数となる。
    • たとえブロックの内と外に同じ変数名を使っていても、それぞれ区別されるのだ。
word = 'local_mac'

['apple', 'orange', 'melon'].each do |word|
  p word.capitalize # "Apple" "Orange" "Melon"が出力される
end

p word # "local_mac"が出力される
  • ブロックパラメータは、カンマで区切って複数指定できる。
(1..10).inject{|a, b| a + b}
ブロックパラメータでない変数のスコープ
  • ブロック内で初めて定義された変数は、そのブロック内でのみ有効な変数となる。
    • ブロックの外では未定義なので、参照しようとしてもエラーになるのだ。
['apple', 'orange', 'melon'].each do |i|
  word = i
  puts word.capitalize
end

p word # NameError: undefined local variable or method `word' for main:Object
  • ブロックの外で定義済みの変数は、ブロック内でも有効な変数となる。
    • ブロックの内と外で同じ変数名を使っていると、同じ変数と解釈される。
word = 'local_mac'

['apple', 'orange', 'melon'].each do |i|
  word = i
  puts word.capitalize # "Apple" "Orange" "Melon"が出力される
end

p word # melonが出力される
  • ブロックパラメータの最後をセミコロンで区切ると、ブロック内でのみ有効な変数名を指定できる。
    • ブロックの内と外で同じ変数名を使っていても、ちゃんと区別されるようになった。
word = 'local_mac'

['apple', 'orange', 'melon'].each do |i; word|
  word = i
  puts word.capitalize # "Apple" "Orange" "Melon"が出力される
end

p word # local_macが出力される
  • ブロックローカルな変数は、カンマで区切って複数指定できる。
word = 'local_mac'
foo = 'local_foo'

['apple', 'orange', 'melon'].each do |i; word, foo|
  word = i
  foo = i
  puts word.capitalize, foo
end

p word # local_macが出力される
p foo # local_fooが出力される

Procオブジェクトとは何か?

Procオブジェクト = ブロックをオブジェクト化したもの。

  • ブロックは、Procオブジェクトにすることで自由に存在できる。
    • メソッドの最後の引数の呪縛から解放される。
  • Procはオブジェクトである。
    • 一方、ブロックはオブジェクトではない。
    • ブロック=解釈前の一塊のコード?
  • よって、Procオブジェクトは変数に代入できる。
irb(main):106:0> hello_proc = Proc.new { puts "hello, world!" }
=> #
  • Procオブジェクトは、callメソッドによってその中身を実行する。
irb(main):107:0> hello_proc.call
hello, world!
=> nil
  • ブロックパラメータには、callメソッドの引数が代入される。
    • ブロックパラメータ = ブロックの先頭で、| ... |に囲まれた変数。
    • 以下、変数nameはブロックパラメータ。
irb(main):108:0> hello_proc = Proc.new { |name| puts "hello, #{name}!" }
=> #

irb(main):109:0> hello_proc.call("zarigani")
hello, zarigani!
=> nil

ブロックを受け取るメソッドの実装

  • Rubyを象徴するeachメソッドは、どのように実装されているのだろうか?
  • 現実にはC言語で書かれた組み込みのメソッドかもしれないが、Rubyコードでeachメソッドを想像しながら、my_eachメソッドを作ってみた。
    • RubyにはC言語のようなシンプルなforループが見当たらなかったので、whileループを使ってみた。
class Array
  def my_each(&block)
    i = 0
    while i < self.length
      block.call(self[i])
      i += 1
    end
  end
end
  • eachメソッドと同じように、シンプルに繰り返すようになった。
irb(main):180:0> a = [1, 2, 3]
=> [1, 2, 3]
irb(main):181:0> a.my_each {|i| p i * 10}
10
20
30
=> nil
  • メソッドでブロックを受け取るには、最後の仮引数で変数名の直前に&を付加する。
  • Rubyの仕様では...
    • &に続く変数の中身(あるいは&に続くオブジェクト)は、Procであることを期待して、暗黙のうちにProcに変換する。
    • と同時に、Procオブジェクトの先頭に&が付加されていると、ブロックに展開しようとする。
    • この挙動はまるで、*修飾された変数が、カンマ区切りの引数を配列にまとめる動作と似ている。
    • と同時に、配列の先頭に*が付加されていると、カンマ区切りのコードに展開されるところも似ている。
  • よって、&修飾された変数を見つけると、Rubyは気を利かせてブロックをProcオブジェクトに変換する。
  • そのようにして、ブロックはProcに変換され、めでたく変数に代入されるのだ。
  • my_eachメソッドによって、ループを制御するコードを書く必要がなくなり、メインの処理に集中できる。
  • もしmy_eachメソッドが存在しなかったら、毎回以下のように書く必要があるのだ。
a = [1, 2, 3]
i = 0
while i < a.length
  p a[i] * 10
  i += 1
end
  • 一方、my_eachメソッドがあれば、コードの見通しがこんなに良くなる。
a = [1, 2, 3]
a.my_each {|i| p i * 10}

様々なブロックの受け取り方と実行

  • ところで、実はすべてのメソッドはブロックを受け取る仕組みを備えている。
  • 以下のように、引数なしのメソッドにもブロックを渡せる。(エラーにならない)
  • 渡しても、そのメソッドにブロックを利用する記述がなければ、活用されないだけ。
class Foo
  def method
  end
end

foo = Foo.new
foo.method {p 'hello'} #何も出力されない
  • これまでは、明示的に&仮引数で受け取って、実行した。
class Foo
  def method(&block)
    block.call
  end
end

foo = Foo.new
foo.method {p 'hello'} #helloが出力される
  • 暗黙のままブロックを実行する方法もある。
  • Proc.newに引数が指定されない場合、メソッドに渡されたブロックからProcオブジェクトを生成する。
class Foo
  def method
    Proc.new.call
  end
end

foo = Foo.new
foo.method {p 'hello'} #helloが出力される
  • 明示的に書き直せば、こんな感じ。
class Foo
  def method(&block)
    Proc.new(&block).call
  end
end

foo = Foo.new
foo.method {p 'hello'} #helloが出力される
  • Rubyには、メソッドはブロックをたった1つしか受け取れない、という大前提があるので、
  • わざわざ変数に代入してからProc.new(&block).callするまでもなく、Proc.new.callでいいじゃないか、という発想なのかもしれない。
  • さらに、Proc.new.callは、yieldという1単語に置き換えられる。
class Foo
  def method
    yield
  end
end

foo = Foo.new
foo.method {p 'hello'} #helloが出力される
  • 渡されたブロックを活用するには、Procを作ってそれをcallする、というお決まりの手順しかない。
  • ならば、よりシンプルにブロックの実行を表現するメソッドを作ろう、という発想なのかもしれない。
  • コードの中にyieldを見ると、yieldの部分を渡されたブロックに置き換えて想像している自分が居る。
  • もはや、yieldがブロックを実行するという感覚よりも、ブロックに置き換える目印に見えてくる。


つまり、

  • 渡されたブロックを実行するには、3つの表現方法がある。
    • 明示的に&blockで受け取り、block.call
    • Proc.new.call
    • yield

yieldが最も洗練された表現方法になるのだと思う。(どれを使うかは自由)

callの省略記法

  • ところで、callというメソッド名は、省略可能である。
hello_proc = Proc.new{ |*args| p args }

hello_proc.call('hello') # 基本
hello_proc['hello']
hello_proc.('hello')
  • 但し、引数がない場合でも、省略後の[]あるいは()は必要。
hello_proc = Proc.new{ p 'hello' }

hello_proc.call # 基本
hello_proc[]
hello_proc.()
  • callの省略なんてやめて、もっと洗練されたyieldを使えばいいのに?と思うかもしれない。
  • しかし、yieldが使えるのは、そのyieldを含むメソッドがブロックを受け取る場合だけ。
  • 上記のように、変数に代入されたProcを実行するには、明示的にcallする必要がある。

2種類のProc

実は、Procには2種類ある。ブロック的なProcと、関数的なProcである。

ブロック的なProc
メソッドに渡されたブロックを、メソッド自身に差し込む(追加する・組み込む)ことが目的。
関数的なProc
変数に代入して、独立した関数のように実行することが目的。
  • ここまで探ってきたProcは、すべてブロック的なProcである。
    • Proc.new、あるいは&変数名でブロックを受け取って生成されるProcだった。
  • 一方、関数的なProcは、lambda(ラムダ)によって生成する。
irb(main):001:0> lambda_proc = lambda{ p 'hello' }
=> #
  • Proc.newの代わりにlambdaを使うだけなのだが、
  • Proc.newとlambdaには、決定的な違いがある。
irb(main):002:0> new_proc = Proc.new{ p 'hello' }
=> #
  • Proc.newのProcは、今までどおりの単なるProcであるが、
  • lambdaのProcには、オブジェクトの内容に(lambda)が追記されている。
  • この違いによって、Proc実行時の挙動にも二つの違いが現れる。
ブロックパラメータの数
  • Proc.newは、ブロックパラメータの個数をチェックしない。
irb(main):046:0> Proc.new{ |a,b| p a,b}.call(1,2,3)
1
2
=> [1, 2]

irb(main):047:0> Proc.new{ |a,b| p a,b}.call(1,2)
1
2
=> [1, 2]

irb(main):048:0> Proc.new{ |a,b| p a,b}.call(1)
1
nil
=> [1, nil]

irb(main):049:0> Proc.new{ |a,b| p a,b}.call
nil
nil
=> [nil, nil]
  • lambdaは、ブロックパラメータの数を厳格にチェックする。
irb(main):053:0> lambda{ |a,b| p a,b}.call(1,2,3)
ArgumentError: wrong number of arguments (3 for 2)
...中略...

irb(main):053:0> lambda{ |a,b| p a,b}.call(1,2)
1
2
=> [1, 2]

irb(main):054:0> lambda{ |a,b| p a,b}.call(1)
ArgumentError: wrong number of arguments (1 for 2)
...中略...

irb(main):055:0> lambda{ |a,b| p a,b}.call
ArgumentError: wrong number of arguments (0 for 2)
...中略...
returnの挙動
  • Proc.newは、ブロックを実行する環境のメソッドのreturnとなる。
def proc_new_method
  f = Proc.new { return :block_return }
  p f.call      #ここ以降は実行されない
  :method_end
end

proc_new_method #:block_returnが返る
      • ちなみに、f = Proc.new { next :block_return } とすれば、
      • Proc.newであっても、実行中のブロックのreturnとなる。(lambdaと同じ)
  • lambdaは、実行中のブロックのreturnとなる。
def lambda_method
  f = lambda { return :block_return }
  p f.call      #:block_returnを出力
  :method_end
end

lambda_method   #:method_endが返る


つまり...

  • Proc.newのProcは、メソッドに一連のコードを差し込むことを想定した動きをしている。
  • lambdaのProcは、独立した関数に近い動きをしている。
  • Proc.newは、メソッドへのMixin(ミックスイン)を作る。
  • lambdaは、無名関数を作る。

様々なProcの作り方と実行方法。

Procを作って、実行する、様々な方法がある。まとめてみた。

ブロック的なProc
  • &変数名、あるいは&オブジェクト
    • &に続く変数名には、渡されたブロックをProcに変換して代入する。
    • &に続くオブジェクトを、暗黙のうちにProcに変換しようとする。
def foo(&block)
  block.call
  block[]
  block.()
end

foo{ p 'hello' }
  • Proc.new
    • 引数に指定されたブロックからProcを作る。
    • 引数が指定されていない場合、メソッドに渡されたブロックからProcを作る。
# ブロック引数あり
Proc.new{ p 'hello' }.call
Proc.new{ p 'hello' }[]
Proc.new{ p 'hello' }.()


# ブロック引数なし
def foo
  Proc.new.call
  Proc.new[]
  Proc.new.()
end

foo{ p 'hello' }
  • proc
    • Proc.newと同じ挙動(Ruby 1.9以降)
# ブロック引数あり
proc{ p 'hello'}.call
proc{ p 'hello'}[]
proc{ p 'hello'}.()


# ブロック引数なし
def foo
  proc.call
  proc[]
  proc.()
end

foo{ p 'hello' }
  • yield
    • メソッドに渡されたブロックからProcを作って、実行する。
    • ブロック引数なしのProc.new.callと同じ挙動
def foo
  yield
end

foo{ p 'hello' }
    • ↑ブロック的なProcにおいて、最も洗練された書き方なのだと思う。
関数的なProc
  • lambda
    • 引数に指定されたブロックから、Proc(lambda)を作る。
f = lambda{ |msg| p msg }

f.call('hello')
f['hello']
f.('hello')
    • 引数が指定されていない場合、メソッドに渡されたブロックからProcを作る。(但し、警告が出力される。よって、好ましくない書き方なのだと思う
def foo
  lambda.call
  lambda[]
  lambda.()
end

foo{ p 'hello' } #warning: tried to create Proc object without a block
  • ->
      • lambdaと同じ挙動(lambdaの別記法
      • より関数定義のような書き方に見える。
    • 引数に指定されたブロックから、Proc(lambda)を作る。
f = ->(msg){ p msg }

f.call('hello')
f['hello']
f.('hello')
    • ↑関数的なProcにおいて、最も洗練された書き方なのだと思う。

Symbol#to_proc(Symbolクラスのto_procインスタンスメソッド)

  • 通常、小文字の単語配列から、大文字の単語配列に変換するには、以下のように書く。
irb(main):001:0> ['mac', 'cat', 'dog'].map{ |word| word.upcase }
=> ["MAC", "CAT", "DOG"]
  • ところが、Ruby1.9以降では、よりシンプルに書けるようになった。
irb(main):002:0> ['mac', 'cat', 'dog'].map(&:upcase)
=> ["MAC", "CAT", "DOG"]


&:upcaseで一体、何が起こっているのだろう?

  • これまでの経緯から、Rubyは&に続くオブジェクトはProcであることを期待して、Procに変換しようとする。
  • どのような仕組みでProcに変換するのかというと、&に続くオブジェクトのto_procメソッドを呼び出している。
  • この段階で、to_procメソッドが実装されていないと(例:String)、TypeErrorが発生して、処理は中断してしまう。
irb(main):005:0> ['mac', 'cat', 'dog'].map(&'upcase')
TypeError: wrong argument type String (expected Proc)
  • ところが、Symbolクラスには、およそ以下のようなto_procメソッドがちゃんと実装されている。(と思う)
class Symbol
  def to_proc
    Proc.new { |obj, *args| obj.send(self, *args) }
  end
end
  • :upcase.to_procが実行されると、to_procメソッドのselfは:upcaseになる。
  • よって、:upcaseの部分は、Proc.new { |obj, *args| obj.send(:upcase, *args) }が生成するProcオブジェクトに変換される。
irb(main):002:0> ['mac', 'cat', 'dog'].map(&:upcase)
irb(main):010:0> ['mac', 'cat', 'dog'].map(&Proc.new{ |obj, *args| obj.send(:upcase, *args) })
=> ["MAC", "CAT", "DOG"]
  • さらに、&に続くProcオブジェクトはブロックに復元され、最終的には、見慣れたブロック渡しのmapと同等になるのだ。
irb(main):002:0> ['mac', 'cat', 'dog'].map(&:upcase)
irb(main):010:0> ['mac', 'cat', 'dog'].map(&Proc.new{ |obj, *args| obj.send(:upcase, *args) })
irb(main):011:0> ['mac', 'cat', 'dog'].map{ |obj, *args| obj.send(:upcase, *args) }
=> ["MAC", "CAT", "DOG"]
  • mapは、配列['mac', 'cat', 'dog']の単語を1つずつブロックパラメータに代入して実行する。
    • objには配列の要素が代入される。
    • *argsには何も代入されず、引数なしの展開になる。
  • もし、Stringクラスにも同様のto_procメソッドを追加しておけば...
class String
  def to_proc
    Proc.new { |obj, *args| obj.send(self, *args) }
  end
end
  • &'upcase'でも、大文字への変換が可能になるのだ!
irb(main):035:0> ['mac', 'cat', 'dog'].map(&'upcase')
=> ["MAC", "CAT", "DOG"]

ハッシュ記法

ハッシュ記法にJSONぽい書き方が追加された。

  • 以前からのハッシュ記法
irb(main):001:0> {:apple => 100, :melon => 1000}
=> {:apple=>100, :melon=>1000}
  • JSONぽいハッシュ記法
    • メソッドの引数として指定する場合、より宣言的に見える。
    • キーは必ずシンボルと解釈される。(キーに文字列は指定できない)
irb(main):002:0> {apple:100, melon:1000}
=> {:apple=>100, :melon=>1000}