極めよRuby道

第2回 イテレータあれこれ

後藤謙太郎 <URL:http://www.notwork.org/~gotoken/mag/cmagazine/>

註: この文書は『C MAGAZINE』2000年9月号 に掲載された記事の元となるものに手を加えたものです.

記事中のプログラムを一つずつファイルにしたものもあります(→list)。

中田さんのおかげでNetscapeでも読めるようになりましたが,まだちょっとカッコ悪いです。「CSSの書き方がなっとらん、こう書け」というお便りは歓迎します。

間違いを見つけたら,gotoken@notwork.org宛に御連絡くださると喜びます。

Copyright(c) 2000 by GOTO Kentaro. All rights reserved.


近況イテレータ基本事項並んで走るeach複数個ずつのeach便利なEnumerable興味深い間違いブロックdo .... end と { .... } の違いeach と for の違い複数のブロックを渡せるか与えられたブロックをそのまま渡すMethod オブジェクトブロックの使いみちよく見られる使いみちブロックでHTMLの入れ子を表現するおわりに参考資料謝辞


近況

こんにちは,ごとけんです.みなさんのところでも暑い日が続いていることと思いますが,Ruby界では相変わらず熱い議論が続いております.前回の原稿を書いたあとでRuby 1.4.5がリリースされました.これはおそらく1.4 系の最終版になると思われます*1

Rubyは1.1の頃から,奇数マイナーバージョンを実験的な開発版,偶数を安定版とする習慣になっていて,1.4は安定,1.5は開発となっています.安定版が公開されると次の開発版が始まります.この原稿を書いている時点での最新系列は安定版が1.4.5で開発版が1.5.4(2000-06-28)ですが,すでに1.5系も feature freeze 宣言が出ているので,みなさんがこの記事を読んでらっしゃる頃には,1.5を固定した1.6と次の開発ターンである1.7がはじまっているかも知れません.1.4 から1.6での変更点はいろいろありますが,前回と関連する正規表現のmオプションについて手短に紹介しましょう.

前回,複数行にわたるパターンマッチでpオプションを取り上げましたが,pオプションは将来破棄されることが決まっており代わりに1.5系ではmオプションが導入されています.どちらも複数行にマッチすることには変わりないですが,挙動がやや異なります.pでは,「^」と「$」の意味が行末と行頭ではなく「\A」(文字列のはじめ)と「\Z」(文字列のおわり)と等価でしたがmではこのような読み替えをしません(図1参照).

-- 図1 1.5で導入された //m と //p の違い --

    % ruby     -ve 'p "ab\nac".scan(/^a./p)' 
    ruby 1.4.5 (2000-06-26) [i386-freebsdelf4.0]
    ["ab"]
    % ruby-1.5 -ve 'p "ab\nac".scan(/^a./m)'
    ruby 1.5.4 (2000-06-28) [i386-freebsdelf4.0]
    ["ab", "ac"]
    % ruby-1.5  -e 'p "ab\nac".scan(/^a./p)'
    -e:1: warning: /p option is obsolete; use /m
            note: /m does not change ^, $ behavior
    ["ab"]

このような変更がどのような議論を経た結果なのかを知るには,メーリングリストの過去の記事をひもとくに限ります.俗にbladeと呼ばれるメーリングリスト全文検索サイト[Ha]は要約機能が優れているので活用しましょう.

ところで最近の目立った話題には,本家Rubyを改造し世代別GCを実装した野心的な処理系[ruby-dev:9990]が挙げられるでしょう.効率の改善が期待されるので1.7系では本家に取り込まれる可能性もあるそうで,楽しみです.また,コミュニティの尽力によりAlphaマシンなどの64ビットCPUでの動作もすでに安定しています.どこでもRubyが気分良く使えるようになりつつあるのはうれしい話ですね.

どこでも,といえば,僕もワケあって,ついにWindows2000を購入してしまい, Cygwin上のtelnetdとTeratermProを使った快適な環境をつくりました.ネイティブ環境のプログラミングもしてみたいのですが,僕はWindowsに不慣れなので Cygwin版から入門中です.んで,そのときのちょっといい話.Cygwin環境に JVim*2をソースからインストールしようとしたらMakefileを書き換える必要があったのですが,なにせまだエディタを入れてなかったので,Ruby のワンライナで書き換えました.まさに Rubyさまさまです.

イテレータ

さて今回はRubyのキモのひとつともいえるブロック渡しを取り上げます.ブロックを渡せるメソッドは以前は一般にイテレータと呼ばれていましたが繰り返さないモノにも使われるので*3,現在はブロック渡しとも呼ばれます.しかし,本来の繰り返すイテレータは意義も分かりやすいのでここではやっぱりイテレータから話をはじめましょう.なお,以下の説明ではRuby 1.4.5を仮定します.

基本事項

イテレータについてはRuby本[MI99]の94頁からはじまる2.18節でかなり詳しく解説してありますので,基本事項はそちらに譲り,ここでは使い方のヒントや注意点などに照準を合わせます.

まず,もっとも基本的なイテレータはeachでしょう.eachは各要素についてブロックで指定された処理を繰り返します.リスト2とリスト3はほぼ等価な動作をC言語とRubyで書いてみました.

-- リスト2 配列aの各要素を表示(C言語) --

   for (i = 0; i < N; i++) {
      elm = a[i]
      printf("%d\n", elm);
   }

-- リスト3 配列aの各要素を表示(Ruby) --

   a.each do |elm|
      puts elm
   end

C言語と比べた場合,Rubyのeachは知る必要のない添字を使わなくても良いという点で優れています.一方,ときには添字も必要とすることもあるので, each_with_indexというメソッドも用意されています(リスト4参照).

-- リスト4 each_with_index の効果 --

   a.each_with_index do |elm,i|
     puts "#{i}番目の要素は#{elm}"
   end

また each はArrayだけでなくDir,Hash,IO,Range,String,Struct で定義されており,これらはそれぞれにふさわしい処理を同じような形で書くことが出来るわけです.リスト5はこれらを実演するものですが,下から2行目の「obj.each{|i| p i}」のレシーバobjはさまざまなクラスであるのに全く同じようにして各要素をアクセスできます.

-- リスト5 each.rb: さまざまなクラスの各要素をpで出力 --

   #!/usr/bin/env ruby

   [
     Dir.open("."),                         # ディレクトリ
     {'a' => 1, 'b' => 2},                  # ハッシュ
     open($0),                              # プログラムファイル自身
     4...9,                                 # 範囲
     "abc\ndef",                            # 文字列
     Struct.new("Foo", "a", "b").new(1, 2)  # 構造体
   ]. each do |obj|
     puts "#### #{obj.type} ####"
     obj.each{|i| p i}  # レシーバ obj は毎回違うが同じ書き方で良い
   end

リスト5で出てくるpはデバッグ用のメソッドで途中経過を表示するのに役立ちます.pという名前はgdbに由来するそうですが,デバッグを目的として組み込んである言語を他に知りません.pで印字される文字列は引数にinspect を施した結果です.また pの出力はprint の出力と違いバッファされないなど,デバッグに向いた特徴を持っています.

すこし話は変わりますが,組み込みクラスのうち each を持っているものを探すにはObjectSpace::each_object を使うことが出来ます(リスト 6参照).ObjectSpace はクラスブラウザを作るときや標準添付ライブラリの weakref.rb などで必要なインタプリタの大域的な情報を取得する道具です.また,この each_object もやはりイテレータになっています.

-- リスト6  eachready.rb: each を持っているクラスを列挙する --

   #!/usr/bin/env ruby

   ObjectSpace::each_object do |klass|
     next unless klass.is_a? Class
     if klass.instance_methods.find{|m| m == "each"}
        puts klass
     end
   end

並んで走るeach

eachを使った2つの変種を紹介しましょう.2次元配列の行と列の意味を入れ換えたいことがたまにあります.リスト7のtranspose は配列の配列のメソッドで,

[  ["Alice", "Bob", "Charlie"],
   [80, 70, 60],
   [34, 56, 78]
].transpose

の結果は

[  ["Alice", 80, 34,],
   ["Bob", 70, 56],
   ["Charlie", 60, 78]  ]

となります.よって,次のeachではi,j,k,のそれぞれに a1a2a3 の要素が順々に代入されることになります.

a = [a1, a2, a3]
a.transpose.each do |i,j,k| 
    puts "#{i} #{j} #{k}"
end

ちなみに外部イテレータを使った別の解が[ruby-talk:1749]zipという関数として紹介されています.

-- リスト7  transpose.rb: 配列の配列の転置 --

   class Array
     def transpose
       res = []
       collect{|a| a.size}.max.times do |i|
         res.push collect{|a| a[i]}
       end
       res
     end
   end

複数個ずつのeach

リスト8は,ある配列に対してイテレータの変数がn個与えられたときに,先頭から順にn個毎の要素を代入するようなeach_chunkです.たとえば,

a = [1,2,3,4,5,6,7,8]
a.each_chunk do |i,j,k|
  p [i,j,k]
end

[1, 2, 3]
[4, 5, 6]
[7, 8, nil]

をプリントします.

-- リスト8 each_chunk.rb: 複数個ずつのeach --

   class Array
     def each_chunk(&block)
       arity = block.arity
       if arity > 0
         i = 0
         while i < size
           yield(*self[i, arity])
           i += arity
         end
       else
         each(&block)
       end
     end
   end

each_chunkで使われているメソッドarityは引数の個数を返すものでProcMethod(後述)にあります.より詳しくいえば,次の関係式を満たします.

foo.arity == if fooの引数の個数が固定
               引数の個数
             else
               -(最低限必要な引数の数+1)
             end

便利なEnumerable

each に相当する万能イテレータをまさに each という名前で定義すると,Enumerableの恩恵を受けることが出来ます.リスト9では Triple(三つ組)というクラスに each だけを定義し,その他の機能は Enumerable を mix-in することで実現しています.リスト9から分かるように上で登場した each_with_index も実はEnumerableで定義されています.

-- リスト9  triple.rb: Enumerable のご利益 --

   #!/usr/bin/env ruby

   class Triple
     include Enumerable

     def initialize(e0, e1, e2)
       @e0, @e1, @e2 = e0, e1, e2
     end

     def each
       yield @e0; yield @e1; yield @e2
     end
   end

   t = Triple.new("だちょう", "うし", "しっぽ")

   # 添字つきeach
   t.each_with_index{|e,i| p [i, e]}
                   #=> [i, "だちょう"], [2, "うし"], ...

   # /ぽ/ にマッチする要素からなる配列
   p t.grep(/ぽ/)  #=> ["しっぽ"] 

   # ソート
   p t.sort        #=> ["うし", "しっぽ", "だちょう"]

   # 配列にする    
   p t.to_a        #=> ["だちょう", "うし", "しっぽ"]

   # to_a がこっそり使われる多重代入
   a, b, c = *t    #=> a = "だちょう", b = "うし", c = "しっぽ"

興味深い間違い

以前,イテレータと関連したなかなか興味深い間違いを見たことがあります.リスト10がその抜粋で,書いたひとの意図と異なる方法で意図した結果を出力していました.このプログラムは "b" を出力するものですが,問題は eachのブロックが何回実行されるかです.aの要素数が3だから3 回と思わせて,0..3は終端の3も含むから0,1,2,3で合計4回というのが答えかというと,それも違って,驚くなかれ正解は1回です.理解できない人は, eachブロックの中に「p i」という1行を追加して,i に渡されるものを確認してください.また分かった人は,リスト10 の #1 から #2 までを一つのメソッドで書く方法を考えてみてください.一方この回数が1回であることが分かった途端に今度はなぜ "b" を出力するのか分からなくなった人はArrayクラスのマニュアルをじっくり読んでみましょう*4

-- リスト10  howmany.rb: eachのブロックは何回まわるか? --

   #!/usr/bin/env ruby

   a = ["a", "b", "c"]
   column = []               #1

   [0..a.size].each do |i|
     column[i] = a[i]
   end                       #2

   p column[1]  #=> "b"

ブロック

do .... end と { .... } の違い

ブロックは do .... end もしくは { .... } のいずれかで与えますが,結合強度が異なります.

次の2つは互いに等価です.

foo bar do .... end
foo(bar) do .... end

また次の2つも互いに等価です.

foo bar {....}
foo(bar{....})

一方foobarの両方にブロックを渡す場合,

foo(bar{....}){....}

を次のように書くことは出来ません.

foo bar {....} {....} #! エラー

ただし次の書き方は許されます.んが,お勧めというわけではありません.

foo bar {....} do .... end

またこれと関係してハッシュのリテラル引数として渡す場合は引数を囲むかっこを省略することは出来ません.

p {1=>2}  # {1=>2}はブロックと
          # 解釈されてエラー

そこで問題,デバッグプリンタpを使った次の #1 と #2 は何を出力するでしょう? 答えは各自確かめてみてください.

def foo(*x); [x, iterator?]; end
p foo do 1 end   #1
p foo {1}        #2

each と for の違い

Rubyにもfor文がありますが,これはeachを呼び出します.すなわち,次の二つはほぼ等価です.

obj.each do |i| .... end
for i in obj; .... end

異なるのは,スコープにまつわる点です.ブロック変数やブロック内で初めて出てきたローカル変数はブロックの外側では無効です.そのため,リスト11 は#!のところでNameErrorを起こします.

-- リスト11 for-each.rb: ブロックとスコープ --

   #!/usr/bin/env ruby

   a = [1,2,3]
   for i in a    # i は初出
     k = i       # k も初出
     p k
   end
   p i           # ここで,i は有効
   p k           # k もまた有効

   a.each do |j| # j は初出
     m = j       # m も初出
     p m
   end
   p j           #! j は無効なので例外
   p m           #! これも例外

複数のブロックを渡せるか

ところで,ブロックは1つしか与えられないのが不便に思うこともたまにあります.例えば木を根からなぞる(traverse)ときは,子に行く前にやる処理と,子から返ったときにやる処理の両方を指定したいのが人情でしょう.こんな場合はあきらめてふたつのProcオブジェクトを渡すというのも手ですが,ここではややトリッキーな方法を紹介します.

やり方としては,イテレータに渡すブロックの中だけで使える特別なメソッド PREPOSTを用意し,これらのメソッドで子に行く前の処理 (@pre)と子から戻ったときの処理(@post)を登録します.このようなことを実現する一つの方法はinstance_evalの利用です.

リスト12ではinstance_evalを使ってプライベートメソッドPREPOSTtraverseのユーザに,事実上公開しています.あまり良くないことかも知れませんが,イテレータ変数を何度も登場させるのがイヤなので*5,僕はときどきこうします.PREPOSTは与えられたブロックをそれぞれ @pre@postに代入するだけのメソッドとなっています.利用例として木をインデントして表示するためのコードをつけていますのでお試しください.

-- リスト12  tree.rb: 2つのブロックを渡す --

   #!/usr/bin/env ruby
   class Tree
     def initialize(val, *children)
       children = children.collect{|c|
         if c.is_a? Tree then c else Tree.new(c) end
       }
       # 第2引数以降が子として登録される
       @val, @children = val, children
     end

     attr_reader :val

     def traverse(&block)
       instance_eval(&block) # @pre と @post を設定
       @pre.call if @pre
       @children.each{|c| c.traverse(&block)}
       @post.call if @post
     end

     private
     def PRE(&block);  @pre = block;  end
     def POST(&block); @post = block; end
   end

   # 例: 木をインデントして表示

   t = Tree.new("a", "a1", Tree.new("a2", "a21", "a22"), "a3")

   depth = 0             # インデントのための深さ

   t.traverse do |n|
     PRE do
       print "  "*depth  # インデント
       puts n.val
       depth += 1        # 子に行く前にdepthを増やす
     end

     POST do
       depth -= 1        # 子から戻ったらdepthも元に
     end
   end

なお,instance_evalに渡されるブロックのブロック引数にはレシーバ自身が渡されます.

与えられたブロックをそのまま渡す

与えられたブロックをそのまま別のメソッドに渡すにはリスト12でやっているように

def method1(arg1, ...., argn, &block)
   ....
   method2(&block)
   ....
end

の要領で書きます.なお,Procオブジェクトはアンパサンド「&」を頭につけて最後の引数に指定すればブロックとして渡ります.

def foo; yield; end
one = Proc.new{1}
p foo(&one) #=> 1

Method オブジェクト

本題とはズレますが,Procが出てきたついでに,Methodオブジェクトを紹介します.Object#methodというメソッドがあり,これは引数で指定されたシンボルに対応するメソッドをオブジェクトとして返します. Objectのメソッドなので,全てのオブジェクトで使えます.

plus1 = 1.method(:+)
p plus1.type     #=> Method
p plus1.call(3)  #=> 4

MethodオブジェクトはProcと同じようにcallで使うことが出来ます.ただし,このオブジェクトは生成したときのレシーバがずっと結び付いています.あまり使うことはありませんが,Rubyでメソッドをオブジェクトにする唯一の方法なので知っておいて損はないでしょう.

ブロックの使いみち

Ruby 1.4.x まではブロックつきで呼ばれたかどうかを調べる関数として iterator?だけが提供されて来ましたが,iterateしないのに iterator?で検査するのはキモチワルイという理由から,Ruby 1.5 以降ではblock_given?という関数も提供されるようになりました.

よく見られる使いみち

このようにブロックの使い道は別にイテレータに限られるわけではありません.例えば上のリスト12で出てきたinstance_evalなんかは繰り返さないわけです.大まかにいって,次のような用途に使われているようです.

繰り返し

各種 eachString#each_byte など.おのおのの要素について行う処理をブロックで渡すのが目的です.イテレータと呼ばれるのがこれです.

手続きをパラメータとして渡す

Array#delete_ifEnumerable#grepString#gsubなど.条件や加工法を指定するのが目的です.

コールバック登録

BEGINENDtrap など.これらはある条件を実行中に満たした場合の処理をブロックで指定します.

環境の変更

module_evalinstance_eval など.別のバインディングで実行することを目的としています*6

制御構造として

Thread::newtimeoutMutex#synchronize など.これらは制御を提供します.

また,もっと詳しい分類があおきさん[Ao]によって与えられていますので,興味のある方はそちらも御覧ください.

ブロックでHTMLの入れ子を表現する

上記以外の用法ももちろんあります.cgi.rb に含まれるCGIのメソッド htmlbody などのメソッド群はHTML要素に対応し,HTMLを表現するためのメソッドで,ブロックを使って要素の入れ子構造を表すのに用いられています.リスト13はcgi.rb のコメントからの抜粋で,これ自身CGIとして機能します.CGI.newの引数「"html3"」はHTML3.2として扱うことを意味します.

-- リスト13  showall.cgi: ブロックによるHTMLの入れ子 --

   #! /usr/env/bin ruby
   # extracted from cgi.rb

   require "cgi"

   cgi = CGI.new("html3")  # add HTML generation methods
   cgi.out{
     cgi.html{
       cgi.head{ cgi.title{"TITLE"} } +
       cgi.body{
         cgi.form{
           cgi.textarea("get_text") +
           cgi.br +
           cgi.submit
         } +
         cgi.pre{
           CGI::escapeHTML(
             "params: " + cgi.params.inspect + "\n" +
             "cookies: " + cgi.cookies.inspect + "\n" +
             ENV.collect{|key, value|
               key + " --> " + value + "\n"
             }.to_s
           )
         }
       }
     }
   }

ところでCGIといえばcgi.rbを使ったCGIは,コマンドラインからデバッグできるほか,同一のCGIプログラムをmod_ruby[MR]がインストールされた Apacheとそうでないサーバの両方で使える点でも優れています.mod_rubyは前田修吾さんの作でApacheに組み込むと10倍以上速くなる場合も報告されており [Ko99],頻繁にアクセスされるCGIを持っている人には大変お勧めなのです.またmod_rubyはRubyをテキストに埋め込んだ言語eRuby[RE]をサポートしており,cgi.rbとmod_rubyの組合せは「HTMLくらい素手で書かかせろ」という向きにもとりわけ優しいものになっています.Rubyを使ったWebアプリケーションについてはいずれ詳しく取り上げたいと思っています.

おわりに

今回はブロック渡しにまつわるあれこれを紹介しました.イテレータもしくはブロック渡しはRubyのもっとも楽しい部分の一つで,応用範囲もさまざまです.そのため「これがイテレータの使い方だ!!」てな感じのものをお見せするのが難しかったのも事実です.やはり使って覚えていくに勝ることはないと思います.今回も登場したプログラムリストはすべて <URL:http://www.notwork.org/~gotoken/mag/cmagazine/>から入手できます.

参考資料

[Ao]

Minero Aoki, 『極める・イテレータ』, <URL:http://www1.u-netsurf.ne.jp/~brew/mine/ja/ruby/iter.html>

[Ha]

原信一郎, "blade/ruby", <URL:http://blade.nagaokaut.ac.jp/ruby/> (<URL:http://www.ruby-lang.org/>の「メーリングリスト」からも行けます)

[Ko99]

小林栄, "mode_ruby" (『日記または随筆』に収録), <URL:http://www.ksky.ne.jp/~sakae/99d/1999-1107.html>

[MI99]

まつもとゆきひろ, 石塚圭樹, 『オブジェクト指向スクリプト言語Ruby』, アスキー出版局 (1999)

[MR]

Shugo Maeda, "RAA entry - mod_ruby", <URL:http://www.ruby-lang.org/en/raa-list.rhtml?name=mod_ruby>

[RE]

TAKEUCHi Kahori, "Ruby Earrings -- eRuby + Ruby CGI", <URL:http://kahori.com/ruby/earrings/>

[ruby-dev:9990]

Masato KIYAMA, "Generational GC", <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/9990>

[ruby-list:13168]

WATANABE Hirofumi, "Re: Copy array (Re: 全角文字列の分割方法について)" <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/13168>

[ruby-talk:1749]

Yukihiro Matsumoto, "Re: Ruby <=> python", <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/1749>

謝辞

前回もそうだったんですが,この記事はメーリングリストでの議論によるところが大です.Rubyコミュニティのみなさんに感謝します.原さん,今回も blade にはお世話になりました.bladeなしではこの記事は書けませんでした.原稿にまで目を通して下さった,まつもとゆきひろさん,お礼の言葉がみつかりません(今度会う時までにお子さんの好きなものを教えて下さい).前回から C MAGAZINE 編集部には有馬さんを通じてWebでの同時公開を快諾していただきありがたく思っております.

Web で記事を同時公開させていただいている最大の理由は,連載内容に関する御意見をいち早くいただきたいからです.そんなわけですので,連載内容に関する御不満,御注文,御提案などございましたら,遠慮無く gotoken@notwork.org までお申しつけ下さい.ただしお返事は必ずしも差し上げられませんことをあらかじめ御了承下さい.ごめんなさい.

そうそう,この原稿はここでも読めるんですけど, C MAGAZINE という雑誌は連載記事を Web で同時公開することをポンと許してくれることからも分かるように知識共有を本位とするとっても素晴らしい雑誌です.まだ持ってない人はぜひぜひ買って下さい.これは筆者からのお願いです.


[Ruby] Rubyどうでしょう
著者: 後藤謙太郎
御意見,御感想,御批判の宛先: gotoken@notwork.org


*1 この号が出る前に 1.4.6 が出ちゃいました :-)
*2 viエディタのクローンVimの日本語化版
*3 itarator は繰り返す奴という意味
*4 それでも分からない場合は[ruby-list:13168]に解題があります
*5 プライベートにせず,またinstace_evalでなくyieldを使い,ブロック内でn.PREと書けば同等のことが出来ます
*6 evalはブロックをとりませんが,第2引数にbindingで生成されるバインディングを渡すことが出来ます