$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
から struct BLOCK が取り
出されて通常のイテレータと同じ状態になる。これが繰りかえされるのでオー
バーヘッドは Proc
の生成分だけになる。しかしこのオーバーヘッドはあくま
でメソッド呼び出しの回数だけに依存し yield
の回数とは関係ないので、大
勢には影響を与えないのだ。
イテレータの使われ方を分類してみた。ただしこの分類はあいまいだし、重な る部分がある。つまり、ひとつのイテレータが二つ以上の分類にあてはまる可 能性があるということだ。絶対的なものではなく、自分がイテレータを定義す る際のひとつの基準として使ってほしい。
ところでどうしてイテレータを分類する必要があるのだろうか? それはこの シリーズが完結した時にわかるだろう。たぶん。
典型的な実例は Array#each
、Hash#each
、String#each_byte
など。
コンテナオブジェクトが、すでに自分の保持しているオブジェクトに対しての アクセス手段を提供する。最も原始的なイテレータの用法でもある。引数を破 壊的に変更すると要素自体が変更されるようになることが多い。
典型的な例は Array#each_index
、String#each
、Integer#upto
など。
オブジェクトを次々にブロックにあたえる点は要素アクセス型と同じだが、そ
のオブジェクトが「作られた」ものであることが違う点。例えば、
Array#each_index
はブロックの引数は Array
に関係してはいるけ
れども、持っているわけではない。それゆえ、要素アクセス型とは
違って、引数のオブジェクトを破壊的に変更しても副作用がおきないことが多
い。このタイプのメソッドはたいていユーザレベルでも定義できるのだが、ユー
ザの簡便のために提供されている、すなわちユーティリティである。
典型的な例は IO#open
、timeout
など。
イテレータブロックの実行中だけ特定の環境を設定する。必然的にこのタイプ
のイテレータは「一回だけくりかえされるイテレータ」であることが多い。開
始と終了があるものにはほぼまちがいなくこのイテレータが適用できる。特に
終端が必須であるものには、ensure
とくみあわせて確実に終端が行われるイ
テレータを提供するとよい。
ちなみに C の malloc
なんかは範囲型イテレータにできてもよさそうである。
考えるだけ無駄か。
典型的な例は signal
、Gtk::Widget#signal_connect
など。
いわゆるコールバックルーチンの登録である。このタイプに関しては、次節 「メソッドコールとの比較」も特に参照されたし。
典型的な例は Enumerable#collect
、Array#delete_if
など。
オブジェクトをパラメータ化したい時は引数を渡す。一方、コードをパラメー タ化したい時はイテレータを使ってブロックを渡す。テンプレートメソッドと かストラテジーと呼ばれるものがこの範疇である。このタイプはブロックの返 り値に意味があることが多い。
実例は IO#each
、SMTP#sendmail
など。
結果全部を一気に返すとでかすぎて危険な時に、文字列(とか)を少しづつ渡す
タイプ。IO#each
はどっちかというと行に分割するほうが目的だと思うが、そ
ういう目的にも(確実ではないが)使えるので挙げてみた。このタイプの特徴と
して、ブロックの引数を再現できないことが多い。
典型的な例は module_eval
と instance_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
とレシーバオブジェクトの対で代用可
能である(もちろんメソッドオブジェクトでもいい)。つまり、yield
を
recv.__send__(mid)
でおきかえるということだ。もともとイテレータはコー
ルバックルーチンとの比較がされたりするのでこれは当然なのだが、どちらを
使えばいいのか迷うことはないだろうか。「迷うことはない、イテレータの方
が手軽だからイテレータだ」という考えもあるだろうが、しかし実際にコール
バックの方がいいこともあるのだ。
イテレータよりもメソッドコールバックの方が優れている最大の点は、「同じ
動作を複数の場所で使うことが容易」ということだ。「持ち運び」できると言っ
てもいい。もちろん Proc
を作って渡してもいいが、それだとクラスを作るの
とさほど手間が違わなくなってしまう。
で、結局イテレータを使うかどうかのさかいめはどこかというと、「ブロック の動作をパラメータ化する必要があるかどうか?」にあると思う。例えば、ブ ロックの動作に必要なデータ(リソース)をあるところでは変えたいとか、動作 をちょっとだけ変えたいとか。そういう要求はイテレータブロックではかなえ られないことだ。つまり、「(イテレータ)ブロックはカスタマイズできない」 ことがポイントといえよう。