Kamuycikap - SentenceDataBase

日々の勉強の記録を気分で書き綴るブログ

繰り返し

Rubyには豊富な繰り返し機構が用意されている。
while及びuntil制御構造は、どちらも事前判定ループで、他の言語と同様の動作をする。
whileではループの継続条件を指定し、untilでは終了条件を指定する。
ifやunlessのように「装飾子」形式で使われる場合もある。
また、loopと言う無限ループの組み込みメソッドもあり、Rubyの繰り返しには欠かせないイテレータは様々なクラスに関連付けられている。

listが次のような配列であるということを前提にして、以降のコードを実行する。
すべて同じ配列にアクセスし、すべての要素を出力する内容である。

list = %w[alpha bravo charlie delta echo]
# ループ1(while)
i=0
while i < list.size do
  print "#{list[i]} "
  i += 1
end

# ループ2(until)
i=0
until i == list.size do
  print "#{list[i]} "
  i += 1
end

ループ1とループ2は「標準形式」のwhileループとuntilループ。
どちらも実質的に同じ動作になるが、条件は相互に逆になる。

# ループ3(事後判定while)
i=0
begin
  print "#{list[i]} "
  i += 1
end while i < list.size

# ループ4(事後判定until)
i=0
begin
  print "#{list[i]} "
  i += 1
end until i == list.size

ループ3とループ4は、ループ1とループ2の「事後判定」。
判定はループの先ではなく後ろで行われる。
このコンテキストでbeginとendを使うのは、応用的なやり方である。
本来であれば例外処理に利用するbeginーendブロックが使われている理由は、その後ろでwhile及びuntilが利用されているから。

# ループ5(for)
for x in list do
  print "#{x} "
end

# ループ6('each'イテレータ)
list.each do |x|
  print "#{x} "
end

ループ5とループ6は、おそらくこのループの一番適切な書き方である。
このループは他のループに比べて簡潔に記載されている。
明示的な初期化もなければ、明示的な判定やインクリメントも無い。
その理由は、配列が自らのサイズを"知って"おり、イテレータのeachがそうした細かい部分を自動的に処理してくれる。
ループ5は単純にこの同じイテレータであるeachを間接的に参照しているに過ぎない。
forループはeachの呼び出しの簡略表現に過ぎない。
別の構文形式よりも便利な代替手段を提供すると言う意味で、syntax sugar(シンタックスシュガー)と呼ぶ。

# ループ7('loop'メソッド)
i=0
n=list.size-1
loop do
  print "#{list[i]} "
  i += 1
  break if i > n
end

# ループ8('loop'メソッド)
i=0
n=list.size-1
loop do
  print "#{list[i]} "
  i += 1
  break unless i <= n
end

ループ7とループ8では、どちらもloop構造を利用している。

# ループ9('times'イテレータ)
n=list.size
n.times do |i|
  print "#{list[i]} "
end

# ループ10('upto'イテレータ)
n=list.size-1
0.upto(n) do |i|
  print "#{list[i]} "
end

ループ9とループ10では、配列が数値のインデックスをモツ事を利用している。
timesイテレータは指定された回数だけ処理を実行し、uptoイテレータは指定された値になるまでパラメータを保持する。
この場合にはどちらも適していないループである。

# ループ11(for)
n=list.size
for i in 0...n do
  print "#{list[i]} "
end

# ループ12('each_index')
list.each_index do |x|
  print "#{list[x]} "
end

ループ11は範囲を使って個々のインデックス値を操作するforループである。
ループ12では同様にeach_indexイテレータを使って配列インデックスのリストに繰り返しアクセスしている。

ここに示した例では、while及びuntilループの「装飾子」形式にあまり重点を置いていない。
これらの形式は度々利用され、記述が簡潔になるという利点がある。
次の例はどちらも同じ意味になる。

perform_task() until finished
perform_task() while not finished

ここまで、ほとんど触れられていないが、ループが必ずしも最初から最後まで問題なく実行されるとは約束されていない。
期待通りの回数の繰り返しが実行されない事や、期待通りの方法で終了しない事がある。

ループを制御する手段の一つがbreakキーワードである。
これは上記のループ7とループ8で利用されている。
breakキーワードは主にループを中断する時に使われる。
入れ子になったループでは、最も内側のループだけが停止される。

retryキーワードは、イテレータの中で使われる場合と、例外処理で利用されるbegin-endの中で使われる場合がある。
イテレータの中で使うと、retryはイテレータを強制的に再起動し、イテレータに渡された引数も再評価される。
retryはwhineやuntilでは利用できない。

redoキーワードを利用すると、最も内側のループ末尾にジャンプし、そこから処理を再会できる。
nextはどのループやイテレータでも使うことができる。

このようにRubyに置ける重要な要素としてイテレータは存在する。
Rubyは定義済みのイテレータに加えて、ユーザー定義のイテレータを利用することもできる。

eachメソッドをイテレータとして実装すれば、どのオブジェクトでもforループで使えるようになるので、これには重要な意味がある。
ただし、イテレータに別の名前をつけると、様々な目的に利用することが可能。

例として、次のような事後判定ループを真似た多目的イテレータがあるとする。

def repeat(condition)
  yield
  retry if condition
end

このイテレータが次のような形で呼び出されると、与えられた無名関数(ブロック)がキーワードyieldで実行される。

j = 0
repeat (j < 10) do j+=1; puts j; end

repeat関数のconditionは「j < 10」の条件式が入り、yieldで評価される無名関数(ブロック)は「j+=1; puts j;」となる。
retryを行うことで、conditionも含めて再評価され、repeat関数を再起動する事になる。
ここでconditionはyieldを実行する前に評価されてしまっているので、yieldしてjが10になった段階でも、conditionはまだjは9なので「j < 10」の条件を満たしてしまい、もう一度ループが実行される。
つまり、トータルで11まで実行してしまうことになる。

ここで利用されているyieldには引数を渡すことも可能である。
その例を以下に示す。

def my_sequence
  for i in 1..10 do
    yield i
  end
end

この関数を下記の様に利用する

my_sequence do |x| print x**3,"\n"  end

xの部分が、my_wequence関数内のyieldの引数となっているiの部分となり、do-endの部分はそのままyieldに渡されて実行される。
yieldにて実行されるdo-end部分のxは、引数として渡されたxが評価されるため、1〜10の数値がそれぞれ3乗された結果がprint文にて画面上に表示される。

contentsへ