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 などのメソッドに対して、プログラムの一部(ブロック)を引数のように渡していると考えると理解しやすいかも(僕はそう理解している)。 以下は参考のためにどうぞ。