前田修吾
先日、筆者がメンテナンスしているmod_ruby[1]というソフトウェアの version 0.9.0をリリースしました。mod_rubyは、この連載で以前にも少し触 れましたが、ApacheというHTTPサーバにRubyインタプリタを組み込むためのモ ジュールです。version 0.8.0以降では、RubyでApacheのハンドラを記述するこ とができるような機能を追加してきたのですが、随分不安定な状態が続いてい ました。0.9.0ではかなり安定しているのではないかと思っています。(毎回リ リースした直後にはそう思うのですが。)
ただ、少しだけ言い分けを書かせていただくと、なかなかmod_rubyが安定しな い理由の一因には、Ruby自体のAPIがなかなか安定しないということもありま す。先日もmkmf.rbの仕様が変わって、「ぎゃっ」と叫ぶことがありました。
そもそも、mod_rubyを書き始めたころには、Rubyインタプリタを操作する ためのAPIが十分に揃っていなかったため、手探りで実装しながら、何か困っ たことが起きる度に、「まつもとさん、これこれこういう機能が欲しいんです けど…。」とまつもとさんにお願いしていました。(実は先日もしたばかりで す。) こういう状態ではなかなか安定しないのも無理はありません。(組み込 み用のAPIがどうあるべきかということをちゃんと考えずに、ad-hocな対処ば かりしてきた私が悪いのですが。)たぶん、Rubyを組み込むアプリケーション がもっと増えると、このあたりに関する議論が深まって、APIも充実するので はないかと思われるので、みなさんもどんどんRubyを組み込んでください。
今回はRubyの特徴の一つであるイテレータを取り上げます。イテレータ (iterator)はもともと「繰り返すもの」という意味を持ちます。反復子という 訳語もありますが、Rubyではあまり一般的ではありません。
多くのプログラムの中では、何らかの繰り返し処理が行われます。たとえば、 配列の各要素に対して同じ処理を繰り返し行うようなループは、プログラマな ら誰もが書いた経験があるのではないでしょうか。そこで多くのプログラミン グ言語ではループを構成するための制御構造を提供しています。たとえば、C 言語の場合はwhile文やfor文がそれにあたります。
Cのwhile文やfor文は色々なループを構成できるように、汎用的なものになっ ています。Rubyのwhileも同様です。これは利点でもあるのですが、その反面、 ユーザにループの制御をまかせるため、範囲外のデータにアクセスしてしまう などのミスを起こしやすい、という悪い面もあります。
プログラムでよく使われる(もっともよく使われると言ってもよいかもしれま せん)ループに、配列やハッシュなどの、複数のオブジェクトを格納するオブ ジェクト(このようなオブジェクトのことをコンテナやコレクションと言いま す)などの各要素に対して、同じ処理を繰り返す、というものがあります。 これらのループの制御は、オブジェクトの構造と密接に結びついています。 たとえば、配列の場合は添字を使って各要素にアクセスしますし、連結リスト の場合はまた別の方法が必要になります。(List1・List2) *1
そこで、ループを制御する役割を、オブジェクトを使うユーザの側にではなく、 オブジェクトそのものに対して与える、という考え方が出て来ます。Rubyはオ ブジェクト言語ですから、オブジェクトを操作するためにはオブジェクトにメッ セージを送って(メソッド呼び出しをして)、オブジェクトそのものに適切な処 理を行ってもらいます。ループの制御についても、同じようにオブジェクトに まかせてしまえばよいというわけです。そして、そのための仕組みがRubyのイ テレータです。(実はRubyのイテレータは繰り返し処理に特化したものではあ りませんが、それについては後述します。)
-- List1 配列の各要素に対して繰り返すwhileループ i = 0 while i < ary.length p ary[i] i += 1 end -- List2 連結リストの各要素に対して繰り返すwhileループ link = list.first while link p link link = link.next_link end
Rubyのイテレータは実はメソッドの一種です。普通のメソッドと異なるのは、 引数の他に、ブロックと呼ばれるコードのかたまりを渡すことができる点です。 *2
まずは例を見てみることにしましょう。List3はArray#eachというイテレータ を使って、配列の各要素を出力するプログラムです。ary.eachの後のdoから endまでがブロックです。このブロックによって、各要素に対して実行したい 処理内容を、イテレータに渡すことができます。ブロックの中では、whileルー プと同じように、breakでループを中断したり、nextで次のループに処理を移 すことができます。
Array#eachは配列の各要素に対して、ブロックの中身を繰り返し実行します。 ブロックの中の一番最初に出てくる|item|という部分は、イテレータから配列 の要素を受け取る部分です。
Array#eachではブロックに渡される値は一つだけですが、イテレータによって は複数の値をブロックに渡すものもあります。その場合は|item1, item2,...| のように複数の値を受け取ることができます。たとえば、Hash#eachはブロッ クに2つの値(Hashのキーと値)を渡します。(List4)
逆に値が渡されない場合や、渡される時でも値を使う必要のない場合には、 |...|は省略することができます。
-- List3 Array#each ary = ["foo", "bar", "baz"] ary.each do |item| print item, "\n" end -- List4 Hash#each hash = {"key1" => "value1", "key2" => "value2"} hash.each do |key, value| print key, "=", value, "\n" end
Array#eachとHash#eachは、ブロックに渡される値の数こそ違いますが、すべ ての要素に対してブロックを繰り返し実行するという点で共通しています。 一般に、各要素に対してブロックを繰り返すようなイテレータにはeachという 名前を付けます。(これは言語による強制ではなく、あくまでも紳士協定です。) このため、ユーザはオブジェクトの構造を知らなくとも、各要素に対して繰り 返し同じ処理を行いたい時には、eachというイテレータを呼び出すだけですみ ます。もし、イテレータがなかったら、ユーザはオブジェクト毎に異なる処理 を記述する必要があるでしょう。つまり、Rubyでは、イテレータによって、 繰り返しの制御においても、ポリモルフィズムの恩恵を受けることができるの です。
念のため説明しておくと、ポリモルフィズムというのは、同じメッセージを異 なるオブジェクトに送ると、オブジェクトによって適切な処理が行われること で、オブジェクト指向を特徴付ける重要な概念です。たとえば、文字列や配列 の長さを知りたい時には、str.lengthやary.lengthのように、同じlengthとい うメッセージを送ってやれば、それぞれのクラスに応じた、適切な処理が行わ れます。このように、ポリモルフィズムのおかけで、異なる種類のデータを統 一的に扱うことができるわけです。
すでに説明したようにイテレータもメソッドの一種なので、イテレータによっ てポリモルフィズムの恩恵を受けることができるというのは、当然と言えば当 然ですね。
ここでちょっと脱線して、Rubyのイテレータとは直接関係のない話をします。 実は、Rubyのようなイテレータがなくとも、繰り返しの制御においてポリモル フィズムの恩恵を受けることができます。そのためには外部イテレータという ものを使います。
Rubyのイテレータは、ユーザではなく、イテレータそのものが繰り返しの制御 を行います。このようなイテレータは内部イテレータと呼ばれます。一方、外 部イテレータの場合は、繰り返しの制御はユーザが行わなければならないので すが、その制御の仕方を統一することができます。
たとえば、配列の各要素の対して処理を繰り返すループについて考えてみましょ う。List1を見ると、このようなループで行う処理は、
の3つに分けることができます。これらは一般化すると次のようになります。
そこで、繰り返しを制御するためのオブジェクトを利用することで、これらの 処理を統一的に行うことができるようにします。他の言語では、このオブジェ クトのことをイテレータと呼んだりするのですが、Rubyの場合は、それではま ぎらわしいのでカーソルオブジェクトと呼ぶことにしましょう。
List5はカーソルの利用例です。ary.get_cursorはカーソルオブジェクトを生 成します。 cursor.more?はまだ繰り返すべき要素がある場合は真を返します。 cursor.nextは次の要素を取得し、次回のnextの呼び出しで次の要素を得られ るように内部のカウンタをインクリメントします。配列以外のオブジェクトの 場合には、cursor.more?やcursor.nextの内部の処理は異なるものになります が、カーソルオブジェクトのインタフェイスさえ揃えておけば、ユーザは処理 の詳細を知らなくとも繰り返しを行うことができます。whileを使って繰り返 しを制御するのはユーザですが、繰り返しの制御に必要な処理はカーソルオブ ジェクトにまかせてしまうことができるわけです。
ちなみにArray#get_cursorというメソッドは標準では提供されませんので、 List6に定義を示しておきます。なぜ標準で提供されていないかというと、 Rubyの場合はイテレータがあるので、カーソルはあまり必要とされないからで す。ただ、カーソルでないとできないこともあるのでまったく意味がないわけ ではありません。たとえば、List7のように2つの繰り返しを並行して行うよう な処理はイテレータでは実現できません。
なお、Rubyでは既存のクラスにメソッドを追加できるので、List6のようなこ ともできるわけですが、標準クラスを変更すると他の人にコードがわかりづら くなりのであまりおすすめしません。カーソルを生成するインタフェイスも統 一できることを示すためにこのような例にしましたが、実際に使う場合には ArrayCursor.new(ary)のようにカーソルオブジェクトを生成するようにした方 がよいでしょう。
-- List5 カーソルの利用 ary = ["foo", "bar", "baz"] cursor = ary.get_cursor while cursor.more? obj = cursor.next p obj end -- List6 カーソルの定義 class ArrayCursor def initialize(ary) @ary = ary @idx = 0 end def more? return @idx < @ary.length end def next obj = @ary[@idx] @idx += 1 return obj end end class Array def get_cursor return ArrayCursor.new(self) end end -- List7 c1 = ary1.get_cursor c2 = ary2.get_cursor while c1.more? && c2.more? obj1 = c1.next obj2 = c2.next ... end
eachは、各要素に対して、単に繰り返しブロックを実行するだけですが、イテ レータを使って、もっと複雑な処理を(シンプルに)行うこともできます。 たとえば、Enumerableというモジュールが提供するイテレータは覚えておくと 非常に便利です。
Enumerableは、Arrayを含む多くのeachを持つクラスにインクルードされてい るモジュールで、eachを利用していろいろな機能を実現します。Enumerableが 提供するイテレータは以下の4つです。
detectとfindは名前は違いますが、実体は同じです。名前の通り、あるオ ブジェクトを見つけるために使います。どのようなオブジェクトを見つけ るのかは、ユーザがブロックを使って指定することができます。
List8は配列の中から数字のみ文字列を見つけるプログラムです。do endの代りに{ }を使っていますが、ここではあまり気にしないでください。 (do endと{ }の違いについては次の節で説明します。) detectは各要素に 対してブロックを実行し、ブロックの値(ブロック中の最後の式の値)が真 になった時に、繰り返しを中断してその要素を返します。つまり、detect はブロックの値が真になるような最初の要素を返します。ここでは、 /^\d+$/ =~ xの値が真になった(数字のみだった)場合に、その要素を返し ます。*3
-- List8 detectの例 ary = ["foo", "123", "bar"] p ary.detect { |x| /^\d+$/ =~ x } #=> "123"
selectとfind_allも名前は違いますが、実体は同じイテレータです。 detectと同じように、ブロックの値が真になるような要素を見つけますが、 detectと違って、要素が見つかった時も繰り返しを中断せずに、すべての 要素を見つけ出し、それらを配列で返します。つまり、selectはすべての 要素の中からブロックの値が真になるような要素を選んで、それらの配列 を返すわけです。List9は配列の中から数字だけの文字列を選ぶ例です。
-- List9 selectの例 ary = ["foo", "123", "bar", "456"] p ary.select { |x| /^\d+$/ =~ x } #=> ["123", "456"]
rejectはselectと対になるイテレータです。selectはブロックが真になる 要素を選んで返しますが、rejectは逆にブロックが真になるような要素を 除外し、それ以外の要素だけを配列で返します。ただ、除外するといって も、レシーバのオブジェクトを変更するわけではなく、返り値の配列から 除外するだけなので、注意が必要です。むしろ、ブロックが偽になる要素 だけを選んで返すと言った方がよいかもしれません。List10は配列の中か ら数字だけの文字列以外の要素を返す例です。
-- List10 rejectの例 ary = ["foo", "123", "bar", "456"] p ary.reject { |x| /^\d+$/ =~ x } #=> ["foo", "bar"]
collectとmapも名前は違いますが、実体は同じイテレータです。collect はすべての要素に対してブロックを実行し、ブロックの値を配列にして返 します。reject同様、レシーバのオブジェクトは変更されない点に注意し てください。(Arrayにはレシーバのオブジェクトを変更するcollect!が用 意されています。) List11は、配列のすべての要素に対して、大文字にす る処理をした結果を得る例です。
-- List11 collectの例 ary = ["foo", "bar", "baz"] p ary.collect { |x| x.upcase } #=> ["FOO", "BAR", "BAZ"]
do endと{ }の違いについて簡単に説明します。その違いは文法的なものです。
List12はList8のdetectの例をdo endで書き直したものですが、このプログラ ムは実行するとエラーになります。これは、List12がList13のように解釈され るからです。つまり、この場合、ブロックはary.detectにではなく、pに渡さ れてしまうのです。
一般に、イテレータの戻り値を使わない場合はdo end、使う場合は{ }を利用 する、という使い分けをする人が多いようです。これは以前は文法上List14の ようにdo endを使って返り値を受け取ることができなかったからです。しかし、 今はこのような使い方もできますので、こういったケースでもdo endを使う人 が増えているようです。また、一行で書く場合は{ }を使う、といったルール を決めている人もいるようです。結局、どちらでもよい場合には、好みで選ぶ ことになります。
では、いったいなぜ二通りの書き方が必要なのでしょうか。それはRubyではメ ソッド呼出しの引数の括弧が省略できるおかげで、List8やList12のようなケー スでは、ブロックを渡す相手が二通り考えられるためです。もし文法的に引数 の括弧の省略が許されていなければ、どちらか一方だけでもよかったでしょう。 このように、引数の括弧の省略はRubyの文法を複雑にしている面があるので、 あまり好ましくないのではないかという意見もあります。*4
-- List12 do endが使えない例 ary = ["foo", "123", "bar"] p ary.detect do |x| /^\d+$/ =~ x end #=> LocalJumpError -- List13 List12の解釈 ary = ["foo", "123", "bar"] p(ary.detect) do |x| /^\d+$/ =~ x end -- List14 以前はエラーだった ary = ["foo", "123", "bar"] result = ary.detect do |x| /^\d+$/ =~ x end
さて、ここまでイテレータは繰り返すものであるという前提でお話してきたわ けですが、実は繰り返さないイテレータもあります。イテレータというのは 「繰り返すもの」という意味ですから、これは一見矛盾する表現ですが、この 矛盾の原因はむしろ「イテレータ」という呼称があまり適切でないことにあり ます。
もともと、RubyのイテレータはCLUという言語のイテレータを参考にしている のですが、RubyとCLUの大きな違いは、CLUの場合はイテレータとメソッドはまっ たく別のものであり、イテレータは値を返すことができないのに対し、Rubyの 場合はイテレータもメソッドであり、値を返すことができるということです。 つまり、Rubyのイテレータはもともとは繰り返しに利用するために考案された のですが、仕組み的には繰り返しに特化したものではなく、メソッドにコード を渡してコールバック的な処理を行うための、より一般的な仕組みなのです。 これは、たとえば、C言語なら関数ポインタを使って実現するような処理です。
それでは繰り返さないイテレータの例を見てみましょう。
List15は配列の各要素を長さが小さい順にソートする例です。各要素の比較を 行う方法をブロックで指定しています。Cのqsort()連想される方も多いのでは ないでしょうか。
List16はString#subの例です。String#subは文字列の置換を行うメソッドで、 第1引数に正規表現、第2引数に置換文字列を取りますが、第2引数を省略して 代りにブロックを与えることができます。ブロックが与えられた場合、正規表 現にマッチした部分がブロックに渡され、その部分はブロックを実行した値で 置換されます。List16ではマッチした文字列を大文字化して置換しています。
List17はProcオブジェクトの利用例です。通常のイテレータは、ブロックを実 行しますが、Proc.newはブロックを実行する代りに、Procオブジェクト化して 返します。Procオブジェクトはcallというメソッドを持っており、callメソッ ドを呼び出すことでブロックを実行することができます。Procオブジェクトを 使うと、ブロックの実行を遅延することができるので、たとえば、GUIアプリ ケーションでイベントハンドラを定義するために利用することができます。
-- List15 Array#sort ary = ["abcdefg", "abc", "abcde"] p ary.sort { |a, b| a.length <=> b.length } -- List16 String#sub p "foo: bar baz".sub(/^\w+:/) { |x| x.upcase } -- List17 Procオブジェクト add_proc = Proc.new { |x, y| x + y } p add_proc.call(1, 2) #=> 3
それでは最後にイテレータを定義する方法を説明します。
イテレータもメソッドなので、defによって定義します。問題はブロックを実 行する方法ですが、二通りの方法があります。
1つはyieldを使う方法です。List18はIOから一行ずつ読み込んで、ブロックに 読み込んだ行を渡して実行し、ブロックが返した値を出力するイテレータの定 義です。(peという名前はruby -peから取りました。)ブロックを実行するため にyieldを利用しています。一見するとメソッド呼び出しのように見えますが、 yieldは実際には予約語で、メソッドではありません。yieldは引数で渡された 値をブロックに渡して実行し、ブロックが返した値を返します。
もう1つの方法はブロックを引数でProcオブジェクトとして受け取る方法です。 (List19) メソッド定義の仮引数リストの最後に&変数名(List19の例では &block)と記述することで、ブロックをProcオブジェクトとして受け取ること ができます。ブロックを実行するためにはProc#callを呼び出せばよいわけで す。ただ、この方法の場合は、Procオブジェクトを生成するコストがかかるた め、yieldよりも遅くなります。
Procオブジェクトは、イテレータの呼び出し時に、foo(..., &block)のように引 数の最後に&を付けて渡すことで、ブロックとして渡すことができます。この ため、他のイテレータにブロックをパスしたい場合には、ブロックをProcオブ ジェクトとして受け取っておくと便利です。たとえば、List20は再帰的なイテ レータの例です。Procオブジェクトとして受け取ったブロックを、自分自身の を呼び出す時にそのまま渡しています。このように再帰的なイテレータも簡単 に書くことができます。
-- List18 yieldの利用 def pe(io) while line = io.gets line = yield(line) print line end end pe(ARGF) do |line| line.upcase end -- List19 &blockの利用 def pe(io, &block) while line = io.gets line = block.call(line) print line end end -- List20 再帰的なイテレータ def foreachfile(file, &block) return if /^(\.|\.\.)$/ =~ File.basename(file) block.call(file) if File.directory?(file) Dir.foreach(file) do |f| foreachfile(File.join(file, f), &block) end end end foreachfile("/usr/lib/ruby") do |f| p f end
イテレータを5ページで説明するのはやはり無理があったかもしれません。舌 足らずな説明でわかりにくい部分も多いと思います。何か疑問があったら、 各種書籍を参照したり、ruby-list MLやfj.comp.lang.rubyなどで質問してみ てください。
*1 連結リストはRubyの標準ライブラリには含まれません。
*2 正確にはイテレータとして使われることを意図していないメソッドにもブ
ロックを渡すことができますが、渡すことは出来ても使われないので意味はあ
りません。
*3 /^\d+$/だと"123\nabc"などにもマッチしてしまうので、正確
には/\A\d+\z/にする必要があります。
*4 実を言うと、私
の意見でもあります。