ブロックを使う2つの方法
Rubyのブロックについてもう少し詳細を見てみましょう。Rubyのブロックはメソッド呼び出しに付加できるコードの塊であって,それ自身はオブジェクトではありません(ブロックをオブジェクト化したものがクロージャです)。渡し方も通常の引数とは違います。次のメソッド呼び出しを見てください。
ary.each {|x| puts x}
このコードを観察すると,以下のようなことが分かります。
●aryオブジェクトのeachメソッドが呼び出されている
●通常の引数はない
●ブロックが付加されている
メソッドから渡されたブロックを利用する方法は2つあります。一つは「ブロック引数」でブロックを受け取ることを明示的に宣言する方法,もう一つは予約語yieldを用いる方法です。図6[拡大表示]ではブロック引数を用いています。配列に対する繰り返し処理を進めるeachメソッドが定義されています。
「&block」の部分がブロック引数です。ブロック引数にはeachメソッドに与えられたブロックがクロージャの形で代入されます。ブロックが与えられていないときにはnilが入ります。与えられたブロック引数を呼び出すには図6のようにクロージャのcallメソッドを使います。
一方,図7[拡大表示]はyieldを用いてブロックを処理しています。ブロック引数の場合と比べて,プログラムの外見上の違いは2つあります。
●明示的なブロック引数の宣言がない
●block.callの代わりにyieldが用いられている
yieldの形式では,ブロックを構成する情報は内部スタックに積まれるだけでクロージャ・オブジェクトは割り当てられません。このため,現在のRubyの実装ではほんのわずかだけ高速に実行できます。また,ブロックが与えられなかった時のエラー・メッセージにも違いがあります(図8[拡大表示])*6。
比較すると,ブロック引数版の方には次の3つのメリットがあります。
●ブロックを処理していることが明確に表現できる●ブロックもオブジェクトとして統一的に扱える
●ブロック引数がnilかどうかを調べるだけで,ブロックが与えられているかどうかを判別できる
一方でyield版にはメリットが2つあります。
●ブロック割り当てが行われないので現在の実装上では少しだけ高速●エラー・メッセージが分かりやすい
なお,yield版でブロックが与えられているかどうか判定するためには,次のように記述します。
defined? yield
結局,ブロックとは何なのか
Rubyのブロックについて,まとめると次の3点になります。
●メソッド呼び出しにコードの塊を渡すことができる
●メソッド側では受け取ったコードの塊を「呼び返す」ことができる。コードの塊の実行が終わると制御はメソッド側に戻る
●ブロックの塊で一番最後に評価された式の値がブロックの値になる。メソッド側はその値を受け取ることができる
ブロックとは,関数1つを採る高階関数を文法的に特別扱いしただけ,とみなすこともできます。しかし,たったこれだけの改良であっても,Rubyのブロックは驚くほど多様な使い方ができます。
繰り返し処理から始まったブロック
最も典型的な利用方法は,他のオブジェクトのコンテナとなるオブジェクトで,要素ごとに処理するメソッドに用いられることでしょう。今回,何度か例として扱った,次のような使い方です。
ary.each {|x| puts x}
Rubyのほとんどのコンテナ・クラスはeachメソッドを持ちますから,上記の方法だけであらゆるコンテナについて要素ごとに繰り返し処理ができます。
eachメソッドはfor文でも用いられています。
for x in ary
puts x
end
この例では内部的にeachメソッドを呼び出し,ary.each…の呼び出しとほぼ同じ動作をします。
元々,Rubyのブロックはこのような繰り返しを実現するために導入されました。ですから,古いドキュメントではブロック付きで呼び出されるメソッドのことを「イテレータ(iterator)」と呼んでいます。iterateとは繰り返すという意味です。しかし,ブロックの応用範囲は当初想定していたものよりもはるかに広く,多様な利用方法の中には繰り返しとは無関係なものがたくさん登場してきました。現在ではブロックをイテレータと呼ぶのは,あまり適切ではないでしょう。
内部イテレータと外部イテレータ
Rubyのブロックのような個々の要素ごとの処理を表現するものをコンテナ・オブジェクトのメソッドに渡し,メソッドが要素ごとの処理を呼び返すタイプの繰り返し方法を「内部イテレータ」と呼びます。
これに対してC++やJavaでイテレータと呼ばれるものは,コンテナの要素を順に取り出すための別のオブジェクトを用意するものです(図9[拡大表示])。このようなタイプの繰り返し方法を「外部イテレータ」と呼びます。外部イテレータ方式では,「コンテナの要素を順に取り出すための別のオブジェクト」のことをイテレータと呼んだり,カーソルと呼んだりします。
内部イテレータは余分なクラスを作らず,使うのも作るのも簡単です。しかし,言語がクロージャをサポートしていないとループ本体と外側とで情報を共有するために工夫が必要になり,C言語の例で見たようにループとしての使い勝手が悪くなります。このため,クロージャを持たないC++やJavaでは外部イテレータが採用されているのです。
一方,外部イテレータ方式では,コンテナとイテレータはお互いに密接な関係を持ちますから,作るのも難しく,利用するためのコード量も少々増えます。外部イテレータ方式を,一行で書けるeachメソッドを用いた繰り返しと比べてみると一目瞭然でしょう。しかし,外部イテレータ方式にも長所があります。複数のコンテナから一つずつ要素を取り出し,並行に処理するような手続きは外部イテレータでは簡単に書けますが,内部イテレータではそうはいきません。
このように外部イテレータと内部イテレータは一長一短です。ただし,言語がクロージャがサポートしているなら,内部イテレータの方が使いやすいでしょう*7。