LoginSignup
22
17

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-04-11

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で書かれているので、ここだけを見ても私には理解できませんでした。

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さんありがとうございます。

22
17
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
17