Edited at

Array#eachからRubyのブロックを理解する

More than 3 years have passed since last update.

RubyのブロックとかProc.newとかがいまいち分かってなかったので調べてみました。


文法上の説明

まずは次のブロックを使った基本的なプログラムを文法的に説明します。

[1,2,3].each do |i|

puts i
end


  • [1,2,3]はArrayクラスのインスタンスオブジェクトです。

  • .eachはArrayクラスのインスタンスメソッドです。

  • do~endはブロックで、メソッドはブロックを受け取ることができます。ただしブロックは引数ではないので、ブロックをカッコで囲むとsyntax errorになりますので注意してください。

  • |i|はブロックの変数(ブロックパラメータ)です。


ブロックの書き換え

ブロックはdo~endでも{~}でもどちらでも書くことができます。一般的に1行で書く場合は{~}を、複数行にわたる場合はdo~endで書きます。最初の例は、以下のように書き換え可能です。

[1,2,3].each { |i|

puts i
}

補足: do~endと{~}は場合によって解釈が異なるので完全に書き換えが可能というわけではないようです。(参考: ブロックをdo…endで書くか{…}で書くかにより挙動が変わる例


Procオブジェクト

ブロックをオブジェクト化したものとしてProcオブジェクトがあります。Procオブジェクトは変数に代入することができるブロックのようなものです。最初の例は、以下のように書き換え可能です。メソッドにProcオブジェクトをブロックとして渡すときは&修飾が必要なことに注意してください。

block = Proc.new {|i| puts i}

[1,2,3].each &block

ちなみに、メソッドはブロックを1つしか受け取れないので、メソッドに複数のブロックを渡したいときは、Procオブジェクトにすることで、メソッド引数として複数のProcオブジェクトを渡すことができます。

また、他の書き方としてKernel.#procメソッドKernel.#lambdaメソッド、そのシンタックスシュガーの->を使っても書くことができます。

block = proc {|i| puts i}

[1,2,3].each(&block)

block = lambda {|i| puts i}
[1,2,3].each(&block)

block = ->(i) {puts i}
[1,2,3].each(&block)

ちなみにprocとlambdaは基本的には同じ動きをしますが、引数の扱いやジャンプ構文(returnとbreak)で動作が異なるようです。

参考: http://docs.ruby-lang.org/ja/2.1.0/class/Proc.html

参考: http://docs.ruby-lang.org/ja/2.1.0/method/Kernel/m/proc.html


ブロックの実行

次にeachメソッドの中でブロックがどのように処理されているのかを再現してみます。eachメソッドに限らず、メソッド内で受け取ったブロックを実行するには2つの方法があります。以下に、2つそれぞれの方法でeachメソッドもどきをArrayクラスに追加してみます。

ちなみに実際のeachメソッドのソースコードは以下のサイト(click to toggle sourceを押すとソースコードが表示されます)で確認できまが、Cで書かれているので、ここだけを見ても私には理解できませんでした。

http://ruby-doc.org/core-2.2.0/Array.html#method-i-each


yield

1つめの方法はyieldを使います。yieldはメソッドに渡されたブロックを実行します。実際にyieldを使ってeachメソッドを作ってみると、次のようになります。(eachメソッドを上書きしようとしたらstack level too deep (SystemStackError)になってしまいましたので、each2という名前にしています。理由が分かる方がいましたら教えてほしいです。[追記: 理由はコメント#2を参照])

class Array

def each2
for i in self
yield i
end
end
end

[1,2,3].each2 do |i|
puts i
end

yieldの部分ではブロックの内部が実行されます。yieldに渡した引数は、ブロックパラメータ(|i|)に渡されます。この引数は複数指定することも可能です。


Proc#call

2つめの方法は、メソッドに渡されたブロックを引数(Procオブジェクト)として受け取り、Procオブジェクトのcallメソッドを使います。ブロックを引数として受け取るには&修飾が必要で、メソッドに1つだけ設定できます。これも同様にeachメソッドを作ってみると、次のようになります。

class Array

def each3(&block)
for i in self
block.call i
end
end
end

[1,2,3].each3 do |i|
puts i
end

callメソッドの部分でブロックの内部が実行されます。callメソッドに渡した引数は、yieldと同様に、ブロックパラメータ(|i|)に渡されます。この引数は複数指定することも可能です。


説明は以上です。

何か間違いがありましたら、ご指摘いただけると幸いです。


1つめのコメントの指摘事項を修正しました。@scivolaさんありがとうございます。