極めるイテレータ

$Id: iterator.rd,v 1.5 2003/06/18 23:28:28 aamine Exp $

Ruby の最大の特徴であり難関でもあるイテレータを極めちゃうぜ講座。

イテレータとはなにか

最初に言っておくと、最近の Ruby 界では「イテレータ」という名称はあんま し好まれてない。くりかえさないイテレータが増えてきたっていうのが理由だ。 ではなんと呼べばいいのかっていうと、「ブロック付きメソッド」とか言うらしい。 どうも好きじゃないなあこの名称。

じゃあどんな名称だったらいいのか。 原点に立ち戻ってイテレータとはなにかと考えると、「制御構造を作るメソッ ド」なのだ。それは以下のような例を見れば感覚的に「あー、そうなのか」と わかる。

def IF( cond )
  yield if cond
end

IF (i > 4) {
  puts 'ok'
}

そんなわけで、原理を考えるとイテレータに代わる名称は「コントローラ」が いいと思うな。もう遅いか。

イテレータと Proc

イテレータと Proc オブジェクトは切っても切れない関係だ。イテレータブロッ クは Proc に変換できるし、Proc をブロックとして渡すこともできる。実装 はともあれ、概念としては両者は同じものだ。どちらも実行コードであり、 (さしかえ可能という意味で)データである。

ところで、「&」引数でイテレータブロックを委譲できるのは常識だけど、 「&」引数での委譲を何重にも使うとブロック呼び出しは遅くなるのだろ うか。ということで試してみると遅くはならない。最適化のところでも書いた ように、「&」引数はイテレータブロックを Proc 化するのでいちおう遅 くはなるのだが、再度「&」で与えると Proc から struct BLOCK が取り 出されて通常のイテレータと同じ状態になる。これが繰りかえされるのでオー バーヘッドは Proc の生成分だけになる。しかしこのオーバーヘッドはあくま でメソッド呼び出しの回数だけに依存し yield の回数とは関係ないので、大 勢には影響を与えないのだ。

イテレータの区分

イテレータの使われ方を分類してみた。ただしこの分類はあいまいだし、重な る部分がある。つまり、ひとつのイテレータが二つ以上の分類にあてはまる可 能性があるということだ。絶対的なものではなく、自分がイテレータを定義す る際のひとつの基準として使ってほしい。

ところでどうしてイテレータを分類する必要があるのだろうか? それはこの シリーズが完結した時にわかるだろう。たぶん。

要素アクセス型

典型的な実例は Array#eachHash#eachString#each_byte など。

コンテナオブジェクトが、すでに自分の保持しているオブジェクトに対しての アクセス手段を提供する。最も原始的なイテレータの用法でもある。引数を破 壊的に変更すると要素自体が変更されるようになることが多い。

アクセスユーティリティ型

典型的な例は Array#each_indexString#eachInteger#upto など。

オブジェクトを次々にブロックにあたえる点は要素アクセス型と同じだが、そ のオブジェクトが「作られた」ものであることが違う点。例えば、 Array#each_index はブロックの引数は Array関係してはいるけ れども、持っているわけではない。それゆえ、要素アクセス型とは 違って、引数のオブジェクトを破壊的に変更しても副作用がおきないことが多 い。このタイプのメソッドはたいていユーザレベルでも定義できるのだが、ユー ザの簡便のために提供されている、すなわちユーティリティである。

範囲型

典型的な例は IO#opentimeout など。

イテレータブロックの実行中だけ特定の環境を設定する。必然的にこのタイプ のイテレータは「一回だけくりかえされるイテレータ」であることが多い。開 始と終了があるものにはほぼまちがいなくこのイテレータが適用できる。特に 終端が必須であるものには、ensure とくみあわせて確実に終端が行われるイ テレータを提供するとよい。

ちなみに C の malloc なんかは範囲型イテレータにできてもよさそうである。 考えるだけ無駄か。

登録型

典型的な例は signalGtk::Widget#signal_connect など。

いわゆるコールバックルーチンの登録である。このタイプに関しては、次節 「メソッドコールとの比較」も特に参照されたし。

パラメータ型

典型的な例は Enumerable#collectArray#delete_if など。

オブジェクトをパラメータ化したい時は引数を渡す。一方、コードをパラメー タ化したい時はイテレータを使ってブロックを渡す。テンプレートメソッドと かストラテジーと呼ばれるものがこの範疇である。このタイプはブロックの返 り値に意味があることが多い。

仕事分割型

実例は IO#eachSMTP#sendmail など。

結果全部を一気に返すとでかすぎて危険な時に、文字列(とか)を少しづつ渡す タイプ。IO#each はどっちかというと行に分割するほうが目的だと思うが、そ ういう目的にも(確実ではないが)使えるので挙げてみた。このタイプの特徴と して、ブロックの引数を再現できないことが多い。

コンテキスト型

典型的な例は module_evalinstance_eval.

このタイプはブロックを eval することが目的といっていい。他の例はあまり 見られないのだが、おれは結構使ってる。使いたくなるのは、例えば「ちょっ とした環境が欲しい」「ここだけで通用するメソッドを定義したい」「使い捨 てにしたいからいちいちクラスは作りたくない」なんて時に有効である。簡単 な使用例をあげよう。

def mkenv( &block )
  Object.new.instance_eval(&block)
end

mkenv {
  def put( msg )
    $stderr.puts msg
  end
  put 'test, test...'
  put 'tadaima maikuno tesuto tyuu.'
}

ブロックが終わってしまえば put はどこにも残らない。まだ GC されてなけ れば ObjectSpace を駆使してなんとかなるがそんなことまで指摘するやつは めったにいないだろう。ちなみにそれすらも通用しないようにするなら mkenv の最後で GC.start すればいいことだ。…本当にやるなよ。

メソッドコールとの比較

ほとんどのイテレータは、メソッド ID とレシーバオブジェクトの対で代用可 能である(もちろんメソッドオブジェクトでもいい)。つまり、yieldrecv.__send__(mid) でおきかえるということだ。もともとイテレータはコー ルバックルーチンとの比較がされたりするのでこれは当然なのだが、どちらを 使えばいいのか迷うことはないだろうか。「迷うことはない、イテレータの方 が手軽だからイテレータだ」という考えもあるだろうが、しかし実際にコール バックの方がいいこともあるのだ。

イテレータよりもメソッドコールバックの方が優れている最大の点は、「同じ 動作を複数の場所で使うことが容易」ということだ。「持ち運び」できると言っ てもいい。もちろん Proc を作って渡してもいいが、それだとクラスを作るの とさほど手間が違わなくなってしまう。

で、結局イテレータを使うかどうかのさかいめはどこかというと、「ブロック の動作をパラメータ化する必要があるかどうか?」にあると思う。例えば、ブ ロックの動作に必要なデータ(リソース)をあるところでは変えたいとか、動作 をちょっとだけ変えたいとか。そういう要求はイテレータブロックではかなえ られないことだ。つまり、「(イテレータ)ブロックはカスタマイズできない」 ことがポイントといえよう。