Ruby でのイテレータを使った繰り返し #1

プログラミング言語 Ruby

プログラミング言語 Ruby

プログラミング言語 Ruby』 を読んで気になったところをまとめてみる記事の第 2 弾。 今回は繰り返しに関して、イテレータについてまとめます。 ちょっと長くなったので #1 (この記事) と #2 に記事を分けます。

イテレータとは

イテレータは英語で書くと iterator であり 「iterate (〜を繰り返し実行する / 反復適用する) するもの」 という意味になります。 すなわちイテレータとは 「何かの処理を繰り返し実行するもの」 です。

イテレータメソッドによる繰り返し

イテレータメソッドとは、メソッド呼び出し時に付属されたコードブロックの処理を繰り返し実行するメソッドです。 例としては以下のものがあります。

3.times { puts "iterator" }
[1,2,3].each { |x| puts x }
2.upto( 10 ) { |i| puts i }
組み込み関数 loop (Kernel::loop メソッド)イテレータメソッドの一種です。 このメソッドは付属ブロックが return や break を実行するまでブロックを繰り返し続けます。
loop_count = 0
loop do
puts "count : " + loop_count.to_s
loop_count += 1
break if loop_count > 10
end
Integer クラスupto メソッドdownto メソッド は、その数値から指定した数値まで数値を増やしながら、または減らしながら繰り返すイテレータです。
1.upto(10)    { |num| puts num } # 1 から 10 までの整数が表示される
10.downto(1) { |num| puts num } # 10 から 1 までの整数が表示される
times メソッド は、その回数分だけ繰り返しを行います。 n.times { ... }0.times(n) { ... } と同じ働きをします。

ここで紹介したほかにも、いろいろな組み込みクラスでイテレータメソッドが定義されています。

独自のイテレータメソッドの定義方法 (ブロック呼び出しの方法)

イテレータメソッドの最大の特徴は、付属ブロックをメソッド内から呼び出すことです。 すなわち、メソッド内から付属ブロックを呼び出す方法を知っていれば、独自のイテレータメソッドを定義できます。 ここではブロック呼び出しの方法を説明します。

メソッド内からブロックを呼び出すのは、yield 文を使うのがもっとも簡単です。 メソッド内で yiled 文を使用すると付属ブロックを実行することができます。 この例では書いていませんが、yield 文の後ろにメソッド呼び出しと同じように引数をつけることで付属ブロックに引数としてオブジェクトの参照を渡すことができます *1

# 付属ブロックを実行するだけのメソッド
def call_block()
yield
end
# 付属ブロックを付けてメソッド呼び出し
call_block { puts "test" }
このメソッドの場合、付属ブロックがあるかどうかをテストせずに yield 文を使っているので、もし付属ブロックがないと LocalJumpError が発生します。
call_block() # 付属ブロックが無いので LocalJumpError が発生
絶対に付属ブロックを付けて呼び出される、という仕様ならこのままでもいいですが、ブロックの有無で動作を変えたいということも多々あると思います。 ブロックの有無は Kernel::block_given? メソッド (別名 iterator?) で調べることができます。
# 付属ブロックの有無で動作変更
def call_block2()
if block_given?
yield
else
puts "ブロックが付属されていません"
end
end
# 実行
call_block2 { puts "ブロック内部" } #=> ブロック内部
call_block2() #=> ブロックが付属されていません

また、仮引数の最後に & 記号を付けた変数をつけておくと、付属ブロックが Proc オブジェクトとしてその引数に格納されます。 yield を使う代わりにこの Proc オブジェクトを実行することでブロックを実行することもできます。

# 付属ブロックを Proc オブジェクトとして受け取る
def call_block3( &block )
# block は付属ブロックをあらわす Proc オブジェクト
# 付属ブロックがない場合は nil
if not block.nil?
block.call
else
puts "ブロックが付属されていません"
end
end

each メソッドと Enumerable オブジェクト

もっとも一般的なイテレータメソッドは each というメソッドです。 Array オブジェクトなど、複数のオブジェクトを内部に保持しているオブジェクトにはたいてい each メソッドが定義されています。 多くの each メソッドは、内部の個々の要素を引数として付属ブロックを実行するようになっています。

[1,2,3].each { |n| puts n }

each メソッドを定義しているクラスは、多くの場合 Enumerable モジュール をインクルードしています。 Enumerable モジュールをインクルードしているクラスのインスタンスを、ここでは Enumerable オブジェクトと呼びます。 Enumerable オブジェクトは、each メソッドを応用したさまざまなメソッドを使用できます。 特に重要なのは以下の 4 つのメソッドです。

collect メソッド (別名 map) は、Enumerable オブジェクトの各要素を引数にして付属ブロックを呼び出し、ブロックの戻り値を配列に格納して返します。

squares = [1,2,3].collect { |x| x*x } #=> [1,4,9]

select メソッドは、Enumerable オブジェクトの各要素を引数にして付属ブロックを呼び出し、ブロックの戻り値が真 (false, nil 以外) となった要素のみを配列に格納して返します。 その逆に reject メソッドは、ブロックの戻り値が偽 (false か nil) の要素のみを集めた配列を返します。

evens = (1..10).select { |x| x%2 == 0 } #=> [2,4,6,8,10]
odds = (1..10).reject { |x| x%2 == 0 } #=> [1,3,5,7,9]

inject メソッドは、少し複雑です。 付属ブロックには 2 つの引数を渡します。 要素の数だけ繰り返し処理を行うわけですが、ブロックの第 1 引数は 1 つ前に実行したブロックの戻り値で、ブロックの第 2 引数は現在の処理対象の要素が渡されます。 繰り返しの初回はブロックの第 1 引数として inject メソッドがあるならばそれを渡し、なければ要素の 1 個目をブロックの第 1 引数に、要素の 2 個目をブロックの第 2 引数とします。 最終的な inject メソッドの戻り値は、最後のブロックの戻り値です。

# 配列に格納された複数の文字列を、ハイフンで繋いで 1 つの文字列にする
puts ["a","b","c"].inject { |str1,str2| str1 + "-" + str2 } #=> a-b-c
# 配列に格納された複数の値から最大の値を得る
puts [1,2,3,4,5].inject { |max,n| max < n ? n : max } #=> 5
# 全要素を足し合わせる
puts [1,2,3,4,5].inject { |sum,n| sum + n } #=> 15

続き

この記事の続きは #2 です。

*1:メソッド呼び出しとは異なり、ブロックが受け付ける引数の個数と渡す個数が異なっていても問題はありません。