«前の日記(2007-10-09 (火)) 最新 次の日記(2007-10-17 (水))»  

まちゅダイアリー


2007-10-11 (木)

PHPer 向け Ruby のイテレータ解説

※ タイトルは masuidrive さんの「PHPer 向け Ruby の基礎」からのパクリです

ちょっと前に、後輩が masuidrive さん主催のRuby on Railsセミナーに参加してきた。 PHP ユーザから見た Ruby の特徴の話だったみたいだけど、一番難しかったのは「イテレータ」のところだったみたい。 「イテレータって、 PHP の foreach みたいなものだよね?」と理解したみたいだったので、即興で foreach との違いを考えてみた。 僕も言語そのものへの理解は深いほうじゃないので、間違いがあるかもしれない(識者のツッコミを期待)。

セミナーの資料は公開されていないみたいなので、代わりに PHPユーザの為のRuby/Rails入門資料を見てみる。 資料では最初に、 Wikipedia のイテレータの説明が引用されてる。

配列やそれに類似するデータ構造の各要素に対する繰返し処理の抽象化である。

うーん。抽象化かぁ。分かったような分からないような。 資料には以下の説明が続く。

ファイル読み込みのように件数が分からないループとかに向いている

繰り返しをforなどではなくメソッドで行う

なるほど。これなら分かる。ということで簡単な例を試してみよう。 以下、話がややこしくならないように、配列に限定して考えることにする。

単純な繰り返し

まずは普通の繰り返しから。配列のそれぞれの値の2倍して出力するプログラム。 each イテレータを使うとこうなる。

arr = [1,2,3,4,5,6,7]
arr.each do |item|
  puts item * 2
end

for を使うとこう。

arr = [1,2,3,4,5,6,7]
for item in arr
  puts item * 2
end

PHPならこう書く。PHP4しか知らないのでPHP5だと違うかも? (Ruby の for とほぼ同じなので、以降は PHP の例は省略)

$arr = array(1,2,3,4,5,6,7);
foreach($arr as $item) {
  echo $item * 2 . "\n";
}

どれもあまり変わらない。だったら別に each のイテレートにするメリットは無いよなぁ…。 (配列以外の独自のデータ型に対して自分でeachメソッドを実装できるってメリットはあるけどここでは省略)

複雑な繰り返し

一歩踏み込んで、配列のそれぞれの値を2倍した新しい配列を作成する処理を考える。 まずは each を使った場合。

arr = [1,2,3,4,5,6,7]
double = []
arr.each do |item|
  double.push(item * 2)
end

これはもちろん for でも書ける。

arr = [1,2,3,4,5,6,7]
double = []
for item in arr
  double.push(item * 2)
end

同じような処理を何度もやりたい場合は、この処理をメソッド(関数)として定義する。 もちろん each と for のどちらを使っても OK 。 なお、以下は意図的に PHP っぽいメソッド名にしている。 (Ruby らしく、Array クラスのメソッドにするのもアリだけど省略)

def array_double(arr)
  double = []
  arr.each do |item|
    double.push(item * 2)
  end
  double
end

arr = [1,2,3,4,5,6,7]
array_double(arr)

もう少し複雑な繰り返し(様々な条件に対応する)

さて、ここからが大切。 配列の値を2倍じゃなくて3倍にしたい場合は、さっきのメソッドを修正して引数に倍数の値 (2とか3とか) を受け取るようにする。

def array_multiple(arr, num)
  new_arr = []
  arr.each do |item|
    new_arr.push(item * num)
  end
  new_arr
end

arr = [1,2,3,4,5,6,7]
multiple = array_multiple(arr, 3)

ここまでは for と each のどっちを使っても一つのメソッドで簡単に対応できる。

でも、2倍や3倍じゃなくて、3を足すとか、5で割った余りをとるとかになると、一つのメソッドじゃ対応できなくなる。 こうなると、別のメソッド (array_plus や array_mod) を作らなきゃいけない。

def array_mod(arr, num)
  new_arr = []
  arr.each do |item|
    new_arr.push(item % num)
  end
  new_arr 
end

このメソッド (array_mod) とさっきのメソッド (array_multiple) を比べてみると、1行を除くと同じ処理(新しい配列を作って元の配列からの計算結果の値を代入)をやっていることが分かる。 これってなんか冗長。 こういう時に同じような処理を一箇所にまとめて、違う箇所だけを書くようにしたいのが、プログラマの性。

そこで登場するのがイテレータ(ブロック)。 これを使えば、「新しい配列元の配列からの計算結果の値を代入」という共通の処理だけをメソッドとして定義できる。 ポイントは yield という文字。

def array_map(arr)
  new_arr = []
  arr.each do |item|
    new_arr.push(yield(item))
  end
  new_arr 
end

そして、2倍にするとか、3を足すとか、5で割った余りにするとかの違う処理は、呼び出し元で書く。

multiple = array_map(arr) {|item| item * 2 }
plus = array_map(arr) {|item| item + 3 }
mod = array_map(arr) {|item| item % 3 }

中括弧の内側に書いたソースが、違う処理に該当する部分になる。 この部分が、 array_map メソッドの yield と置き換わるイメージ。

つまり、 Ruby のイテレータ(ブロック)を使えば、繰り返しの共通処理だけをメソッドにできる。 これが冒頭で紹介した Wikipedia からの引用文「繰返し処理の抽象化」ってことか。なるほど。 ちなみに、 PHP4 でも array_map という関数を使えば似たようなことが実現できるっぽい。 ただし PHP4 の array_map は、「違う処理」の部分を関数として書かないといけない。

さらに多彩な繰り返し

今の例は、ある配列の値に計算を加えて(同じサイズの)新しい配列を作るものだった。 他にも、配列に対する繰り返し処理のパターンはいくつかある。

  • それぞれの配列値から、指定した条件(10以上の値、奇数のみ…などなど)にマッチしたものだけを取得する (array_select)
  • それぞれの配列値を指定した条件(大きい順、小さい順…などなど)にしたがって並び替える (array_sort)

これらの処理もイテレータを使うことで共通処理と個別の条件を分離できる。 これが Ruby のイテレータのメリットじゃないかな。

ちなみに、イテレータの説明のために array_map メソッドを自作したけど、これらのメソッドは Array クラス(がインクルードしているEnumerableモジュール)にてあらかじめ用意されている。なので、自分で array_map メソッドを作らなくてもそのまま以下のように書ける。

arr = [1,2,3,4,5,6,7]

arr.map {|item| item * 3 }
arr.map {|item| item + 3 }
arr.map {|item| item % 3 }

arr.select {item| item > 10 }
arr.sort {|a,b| a[0] <=> b[0] }

まとめ

イテレータを使うことで、配列に対する操作 (値の抜き出しや並び替え)を共通化することができるのがメリット…だと思う。 each, map, select, sort などのメソッドに対して、プログラムの一部(ブロック)を引数のように渡していると考えると理解しやすいかも(僕はそう理解している)。 以下は参考のためにどうぞ。

Tags: Ruby