yunomuのブログ

趣味のこと

2時間でわかる繰り返し

 今回は繰り返しの話です。

 繰り返しというのは要するに同じことや似たようなことを何回もやる、ということですが、そのやり方はたくさんあります。これはRubyだからということではなく、様々なプログラミング言語で様々な繰り返しの方法があります。その多数の中から状況によって適切なやり方を選択できるということが、プログラミングの上達につながるといって良いでしょう。

 ただしこれは非常に難しい。一流と呼ばれるプログラマでも間違えることが多々ありますし、正解が存在しないということもあります。

 なので、ここではとりあえずどういうパターンがあるのかを見て「そういうものもあるんだな」と思っておく、というところから始めましょう。

無限ループ

 まずは無限ループからです。無限ループというのはそもそもプログラミング用語であり、その名の通り、特定の処理を無限に繰り返します。最も一般的な書き方はこうです。

while true
  puts "ainachan"
end

 これを実行すると"ainachan"と連呼しつづける迷惑なやつになります。C-Cで止めてあげましょう。

 原理としては、マニュアルの「制御構造」のページに書いてあります。whileは、その後に続く式がfalseになるまで永遠に繰り返す文ですが、今回の場合は式の部分にtrueと書き込んでいるので、falseになることはありません。つまり無限に繰り返すということです。

 無限に繰り返して終わらないというのは困ったようにも思えますが、普通のプログラムならまあC-Cを入力すれば終わりますし、終わらないということがそれなりに便利なともあります。

 余談ですが、例えば今まさに使っているブラウザとかエディタのようなGUIアプリケーションは、これまでに作ってきたHelloWorldやMegaGreeter(20分ではじめるRuby)などのようにやることやったらすぐ終了してしまうというのでは困るので、どこかしらで無限ループのようなことをしています。OSとかもそうです。

 ただ、普通は終わらないと困るので、終わらせる方法をC-C以外で用意しておくのが普通です。

 例えば、ループするかどうかを判断する変数つづけるを使います。(変数は実は日本語も使えます)

つづける = true
while つづける
  puts "ainachan"
  つづける = false
end

 これを実行するとどうなるでしょうか。while文は、つづけるtrueの間は無限に繰り返しますが、ainachanと言った後でつづけるfalseになるので、そのままwhile文は終了し、1回しか"ainachan"は出力されません。これだとループの意味は無いので、普通はif文をからめてこういう感じになります。

つづける = true
while つづける
  puts "ainachan"
  if 終了条件
    つづける = false
  end
end

 この終了条件の部分には、「どういう時に終わらせたいか」という条件式が入ります。例はちょっと後で。

無限ループ2

 無限ループの書き方についてもいくつかあります。普通は先ほど説明した通りに書くのが良いのですが、こういうパターンもあります。

puts "ainachan" while true

 今度は1行です。これは「while装飾子」というパターンです。先ほどと同じくマニュアルの制御構造のページのwhileの節の次に書いてあります。これはwhileの後の条件がtrueになるまでwhileの前の文を繰り返すという意味です。基本的には1行を繰り返すのですが、複数の行を1行として扱うbeginendと組み合わせると以下のようになります。

begin
  puts "ainachan"
  puts "anchan"
end while true

 ほとんど最初のwhile true 〜 endと同じように動くように見えますが、while装飾子とbegin endを組み合わせた場合だけちょっと違う動きをします。while装飾子のtruefalseに変えてみてください。

begin
  puts "ainachan"
  puts "anchan"
end while false

 条件式がfalseなのでループは実行されないように見えますが、1回だけ実行されます。「1回は少なくとも実行したい」という時はこのような書き方をします。滅多にありませんが、稀にそう思うことがあります。なにかスッキリしない時に思い出すと良いでしょう。

無限ループ3

 untilというのもあります。これは単にwhileの条件のtruefalseが逆になっただけで、ほとんど同じです。until 式の部分はwhile not 式と書けば全く同じなので実のところあまり見かけませんが、でもなにかそれでは気持ち悪い時に使います。

 Rubyにはそういう「同じだけど名前が違う」とか「ほとんど同じだけどすごく細かいところが違う」という機能がたくさんあります。一番気持ちいいやりかたでやると良いでしょう。

4回繰り返す

 4回繰り返します。まずはこうです。

puts "ainachan"
puts "ainachan"
puts "ainachan"
puts "ainachan"

 4回繰り返しました。あまりにも単純ですが、これはこれで正解になることがあります。開発初期のとりあえずやってみようという段階や動作確認ではこういう感じでもよいことがあります。コピペも有効に使っていきましょう。

4回繰り返す (n回繰り返す)

 「4回」というように、回数が決まっている場合はIntegerクラスのtimesメソッドを使うのが簡単です。

4.times {
  puts "ainachan"
}

 これなら100回にしたくなった時にも4の部分を書き換えればよいので簡単です。

 4の部分を変数にするのも良いでしょう。n回繰り返すパターンは以下です。

n = 4 # 今回は4回繰り返します

n.times {
  puts "ainachan"
}

 nをメソッドのパラメータにすると、指定した回数だけ繰り返すメソッドも作ることができます。今日は4回、明日は10回繰り返したい、という場合などに便利です。

def renko(n)
  n.times {
    puts "ainachan"
  }
end

renko(4)

for文を使って4回繰り返す (timesの別の使い方)

 timesにはブロック({ ... })を指定する以外にも使い方があります。

 例えば、以下のようにfor文といっしょに使えます。

for i in 4.times
  puts "ainachan"
end

 ここで、iは、何回目かという情報が入っています。for文の中でputs iなどして確かめてみるとよいでしょう。0から始まり、1, 2, 3と表示されると思います。このように回数は0から始まりn - 1でおわります。そしてこのfor文のコードは以下と同じです。

4.times {|i|
  puts "ainachan"
}

 timesのブロックは、このようにカウンタiを受け取ることができます。ブロックの中でputs iするとまた同じようになるはずです。

 また、for文のパターンは以下のコードとも全く同じです。そもそも実は、このコードの省略形がfor文です。Rubyにはこのような同じ処理の書き換えパターンがよく登場します。くわしくは「制御構造」のマニュアルに書いてあります。ここではeachが出てきます。

4.times.each {|i|
  puts "ainachan"
}

 IntegerクラスのtimesはブロックをつけないとEnumeratorを返すとマニュアルに書いてあります。Enumeratorというのは「なんか数えられるやつ」くらいの意味です。数字だったりリストだったり、そういう何かです。そしてマニュアルで言うとEnumeratorクラスの中にeachメソッドがあります。eachなのでつまり、「それぞれの要素に対してブロックを実行する」という意味で、これが数字やリストに対して実行すると繰り返しになります。

4回繰り返す (カウンタを使う)

 ここで、現在の繰り返し回数iがわかるということは、iを使って以下のように書くこともできます。

100.times {|i|
  if i < 4
    puts "ainachan"
  end
}

 日本語で説明すると、「100回繰り返し、繰り返し回数が4回未満なら"ainachan"と表示する」ということになります。これはこれでいいのですが、普通は100回も繰り返さずに、4回以上になった時点でやめればよいのです。というパターンが以下のコードです。

100.times {|i|
  if i >= 4
    break
  end
  puts "ainachan"
}

 これが「100回繰り返すが、4回以上になったらやめる」です。ループの中でbreakと書くと、breakが実行された時点でループそのものが終了します。「ループを抜ける」とも表現します。このようにループは途中でやめることもできます。

4回繰り返す (whileとカウンタを使う)

 最初に出てきたwhile文を使って以下のように書くこともできます。

i = 0
while i < 4
  puts "ainachan"
  i += 1
end

1. while文にはカウンタが無いので自分でi = 0と定義します
2. iが4未満の場合に繰り返します
3. "ainachan"って表示
4. iに1を足します
5. 2に戻る

 大事なのは2と4です。4で自分でちゃんと数えてあげる必要があります。4が無いと無限ループします。

 また、無限ループとbreakを使って以下のように書くこともできます。

i = 0
while true
  if i >= 4
    break
  end

  puts "ainachan"
  i += 1
end

 このように、whileの条件式をループ内のif文で代用することもできます。

 ただ、これだと条件がi < 4だったのがi >= 4と逆になっていてちょっとややこしいかもしれません。そういう時のために、前にちょっと紹介したuntilや、ifの逆のunlessというものがあります。試しに書いてみるとこうなります。

# unlessとbreakを使うパターン
i = 0
while true
  unless i < 4
    break
  end

  puts "ainachan"
  i += 1
end
# untilを使うパターン
i = 0
until i >= 4 
  puts "ainachan"
  i += 1
end

 どのやりかたが良いかというのは状況によるとしか言えません。大抵は状況により最適な方法は存在しますが、1つとは限りませんし、確固たる理屈があるわけでもなく経験則という場合も多いです。とりあえずは気持ちの良いものを使いましょう。

3回に1回違うことをする

 3回に1回、"ainachan"じゃなくて"anchan"と表示してみましょう。一番簡単なのはこうだと思います。

puts "ainachan"
puts "ainachan"
puts "anchan"
puts "ainachan"
puts "ainachan"
puts "anchan"

 無限にやるならこうです。

while true
  puts "ainachan"
  puts "ainachan"
  puts "anchan"
end

 ここで考えなおしてみましょう。「3回に1回」ということは、ループの回数を数えてカウンタが3の倍数の時だけ表示を変えればよいのです。その実装が以下のコードになります。

i = 1
while true
  if i % 3 == 0
    puts "anchan"
  else
    puts "ainachan"
  end

  i += 1
end

 %というものが出てきましたが、これはmod演算といって、割り算の余りを出す演算子です。要するにifの条件式は「iを3で割った余りが0ならば」という意味になります。つまり「3の倍数ならば」ということです。前と違ってi = 0ではなくi = 1から始まっているのは、最初の1回目で"anchan"と表示しないためです。ちなみに%の詳細はIntegerクラスのマニュアルにあります。

 余談ですが、10年くらい前にはこのようにループとifを使いこなせてmodの意味がわかっていればプログラマとして就職できるという噂がありました。実際どうだったのかはよくわかりません。

リストの中身に従って繰り返す

 1行目は"ainachan"、2行目は"anchan"、3行目は"mendako"と表示しようと思います。これも繰り返しで表現することができます。要するに「表示する」という部分は同じなので、文字列のリストを作って、それぞれに対してputsしてあげればよいのです。というのが以下のコードです。

["ainachan", "anchan", "mendako"].each {|s|
  puts s
}

 ここで[ ... ]の部分はArrayクラスのメソッドです。この["ainachan", "anchan", "mendako"]の部分で配列(Array: リストのようなもの)を作っています。配列にも前に少し紹介したeachメソッドがあり、ブロックの中でそれぞれに対してputsを実行します。sには配列の中身が1つずつ入ってきます。とりあえず書いてみればなんとなくわかると思います。

 これを使って「3回に1回〜」を実現することもできます。こんな感じです。

["ainachan", "ainachan", "anchan"].each {|s|
  puts s
}

 無限にやるならこれ自体をループしてしまえばよいのです。

while true
  ["ainachan", "ainachan", "anchan"].each {|s|
    puts s
  }
end

 無限の場合は、Arrayクラスのcycleメソッドを使って書くこともできます。cycleは配列に含まれる要素を繰り返すメソッドで、["ainachan", "anchan", "mendako"].cycleのように書くと["ainachan", "anchan", "mendako", "ainachan", "anchan", "mendako", "ainachan", "anchan", "mendako", ...]のような感じになります。要するに以下を実行してみましょう。先程のコードと同じ結果になるはずです。

["ainachan", "ainachan", "anchan"].cycle.each {|s|
  puts s
}

 ということはつまり、以下のコードは一番最初の無限ループのコードと同じ結果になります。

["ainachan"].cycle.each {|s|
  puts s
}

 意味は少し複雑になって、「"ainachan"だけが含まれた配列(["ainachan"])を無限に繰り返して(cycle)、その中身を全て(each)表示する(puts)」ということになります。この場合も、どちらが良いのかは状況次第です。

 もちろん、breakと組み合わせて"mendako"がきたらループを抜けるというようなこともできます。

["ainachan", "anchan", "mendako", "kumachka"].each {|s|
  if s == "mendako"
    break
  end
  puts s
}
end

 この場合は"mendako"と"kumachka"は表示されません。

繰り返しの途中でやりなおしたり次に行ったりする

 breakと似た話で、繰り返しの途中で後の処理をせずに次に行きたいことがあります。例えば、以下のコードです。

["aina", "an", "mendako", "kumachka"].each {|s|
  print s
  if s == "mendako"
    puts
    next
  end
  puts "chan"
}

 このコードの意味は、「文字列が"mendako"の時だけ"chan"を付けない」という意味になります。ここでprintは、改行せずに文字列を表示するメソッドです。それと、putsはうしろに何も指定しないと改行だけするメソッドになります。

 "mendako"のif文の中にnextという命令があります。これは繰り返し(eachのブロック)の中の処理を途中で辞めて次の繰り返しに行く、という意味で、この場合はput "chan"を実行せずに次の"kumachka"の処理に移ります。実行してみるとなんとなくわかると思います。

 このnextと似たものとしてredoがあります。こちらは次に行かずに、今と同じものをもう一度やりなおします。つまり以下のようにnextredoに置き換えたものを実行すると、"mendako"を処理しようとしてredoされて再度"mendako"を処理しようとするので無限ループします。

["aina", "an", "mendako", "kumachka"].each {|s|
  print s
  if s == "mendako"
    puts
    redo
  end
  puts "chan"
}

 redoはあまり使うことはないかもしれませんが、もう一度やればうまくいくという状況があれば思い出してください。リトライ(retry)というのもあって似ていますが、たぶんリトライの方が出番は多いと思います。後で説明します。

範囲にしたがって繰り返す

 Rangeについて書こうと思ったけどあまり変わらないのでやめ。(1..4)(1...5)のように数字の範囲を指定することができます。これらはRangeクラスのオブジェクトで、eachメソッドがあるので、(1..4).each {|i| puts i }とかやってみてください。

再帰

 再帰は少し変わったパターンです。うまく使えばわかりやすくなりますが、なかなか難しい概念ですし、性能も良いとは言えないところがあるのでさほど使われることはありません。でも状況によってはすごく有用なこともあるという、そういうものです。そういうのもあるんだなと思っておくだけでも良いでしょう。

 メソッドは自分自身を呼び出すことができます。これを利用して無限ループを書くことができます。

def ainachan
  puts "ainachan"
  ainachan # 自分自身(ainachanメソッド)を呼び出し
end

ainachan

 ainachanメソッドの中でさらにainachanメソッドを呼び出しています。それだけでは最初の1回目が実行されないので、最後にainachanメソッドを呼び出しています。これを実行すると、無限ループの時と同様に"ainachan"と表示され続けるはずですが、続けているうちに途中でSystemStackErrorみたいなエラーが出て止まると思います。これは実行した環境(状況みたいなもの)によって変わるのでなんとも言えませんが、数秒で止まる人もいれば数時間も止まらない人もいると思います。それはまあそういうものです。

 このエラーは自分自身を呼び出す(これを再帰といいます)回数が多すぎた時に起きます。このようなエラーが発生する可能性があるので、再帰はあまり性能がよくないとされています。一応それを克服する方法もありますが、それなりに特殊なことになるので難しい。

 ここで何が起きているか、何故エラーが起きるかというと、Rubyの外の話も色々と関わってくるので、ここでは説明しませんが、そちらの方に興味があればそういう方向に行くのもアリです。(私はそっちの方が詳しいです)

 実のところ、再帰というのはすごく強力な概念なので、ここで説明している繰り返しのほとんどを再帰を使って書き直すことができます。ただ、エラーが起きることもあるように、Rubyを含む多くのプログラミング言語ではあまり効率が良くないので、かなり限られた状況でしか使われません。逆に言うと再帰で書けるコードのほとんどは他の繰り返しに置き換えることができるのです。必要以上に避ける必要もありませんが、繰り返しで書けるならできるだけ繰り返しで書く方が良いでしょう。

リトライ(と例外)

 繰り返すことの目的のひとつとして、エラーが起きたからやり直すというものがあります。つまりリトライです。redoと似ていますが、こちらはコンピュータの本質的な問題に対処するための仕組みです。

 コンピュータには同じ処理でももう1回やれば成功するかもしれないという事があります。例えば外部のデバイスと通信するときです。

 つながっていないハードディスクに書き込もうとするとエラーになりますが、つないでからリトライすると成功するかもしれません。インターネットにつながっていない時に通信しようとするとエラーになりますが、時間が経って再接続された後だと成功するかもしれません。

 このように、プログラムとしては間違いは無くてもコンピュータそのものの状況(故障など)や世の中の状況(当然ながらインターネットを使うプログラムは世界が滅ぶと使えません)によって実行が不可能になるということがあります。これをRubyでは例外(Exception)と呼んでいます。Rubyではエラーも例外の仲間です。エラーと言った時は例外の話をしていると思って問題ありません。前節で出てきたSystemStackErrorもエラーであり、Rubyでは例外の一種です。

 例題として例外を起こすとなるとそれなりの準備もあって面倒くさいので、サンプルコードでは例外を発生させる命令を使います。例外は、一番簡単なものとしては他のオブジェクトと同じくExceptionクラスを使ってException.newで作ることができます。これだけではただのクラスと同じなので、「発生させる」といった場合にはraise命令を使う必要があります。つまりこうです。

raise Exception.new

 この一行をirbなどで実行してみてください。これまで散々見てきたようなエラーが起きた時みたいな表示が出たらたぶん成功です。本当にエラーだったらなんかごめんなさい。

 このraiseというのは、例外を発生させる命令です。あまり使うことはありませんが、プログラムの中でなにか想定外のことが起きた時に使います。存在しないメソッドを呼んだ時や、想定しない使い方をした時などに発生します。例えば、Integerメソッドにはmendakoメソッドは存在していないので2.mendakoはNoMethodErrorという例外が起きますし、1を0で割った時にどうなるかは誰にもわからないので1 / 0はZeroDivisionErrorが起きます。irbなどで試してみてください。

 ちなみにraiseでは適当な文字を指定することもできます。なにか適当な名前を思いつかない時はエラーメッセージを投げておけばOKです(raiseを使うことを「投げる」と言うことがあります)。つまりこういうことができます。

raise "例外が起きたよ"

 Rubyでは、例外が起きた時に何かをすることができます。そして「例外が起きた時にできる何か」の中にはリトライというものがあります。これを使って無限ループを作ることができます。具体的にはこうです。

begin
  puts "ainachan"
  raise "なんかエラー" # 例外が発生
rescue
  retry
end

 これを実行すると無限ループすることを確認できると思います。

 例外(エラー)が起きそうな場所はbegin ... rescue ... endで囲む必要があります。この中のbegin ... rescueの中でエラーが発生するとrescue ... の間のプログラムが実行されます。この場合はretryです。retryは、rescueに対応するbeginまで戻って、そのbeginのところからやり直しますという命令です。つまりここでは、raiseのところで例外が発生するのでrescueの節が実行されてretryの作用でbeginのところまで戻って再びputs "ainachan"が実行されることになります。

 試しにraise "なんかエラー"の行を削除してみてください。例外が発生しなくなるので1回だけ"ainachan"が表示されて終了すると思います。rescue ... endの節は、その前の節で何事も起きなければ実行されることはありません。

 これが何の役に立つのかというと、例えば通信エラーだった場合にはretryの前に再接続のプログラムを実行すれば良いわけです。あまり使うことは無いかもしれませんが、みなさんが使う通信やファイル読み書きのプログラムではライブラリの中のどこかでこのような処理をしています。機会があったあ思い出してみてください。

おわり

 このように、繰り返しひとつとってもプログラムにはやりかたがたくさんありますし、目的もたくさんあります。もちろんここで説明した意外にも様々なパターンがありますが、少なくともRubyにおいてはほとんどがここで挙げたパターンの組み合わせになると思います。

 「色々なやり方があるんだな」とわかった上で、やり方を探しつつ、時にはより良い方法を探してマニュアルを読んだり先人のコードを読んだり詳しい人(私を含む)に尋ねたりして、良いコードを書けるようにがんばってみてください。