大多数のプログラミング言語に while
というループ(繰り返し)のための制御構造があります。
基本的な考え方は
- 条件式と〈本体〉を持つ
- 条件式が真である間,本体を実行(評価)し続ける
というものですね。
ここで〈本体〉と書いたのはループの中身のことです。
Ruby にも制御構造 while
があります。
制御構造 (Ruby 3.0.0 リファレンスマニュアル)
多くの言語と同様,まず最初に条件式を評価して本体を実行するかどうか決めます1。
Ruby の初心者向け記事2の定番テーマの一つですが,そういった記事に書かれているサンプルコードに違和感を持ったのがこの記事を書いたきっかけです。
よくある while のサンプルコード
while
を解説するためのサンプルコードでこういうのがよくあります。
n = 1
while n < 10
puts n
n += 1
end
Ruby の公式リファレンスマニュアルの while
の説明にも似たようなサンプルが掲載されています。
あまり良い例とは言えません。C 言語プログラムの直訳みたいに見えます。
こんな会話を聞かされているようです。
「貴殿は如何にありや。」
「息災なり。貴公に謝す。して貴公は。」
何がいけないのか,どう書くべきか
上記のコードは何がいけないのでしょうか。
第一に Ruby らしくありません。
そして分かりづらい。
私のようなヘタレには,表示される数が 1 から 9 までであるということが一瞬では分かりません(しかし,こういうコードに馴れている人なら一瞬で分かるでしょう)。
この処理は
1.upto(9) do |n|
puts n
end
とか
(1..9).each do |n|
puts n
end
などと書くべきです3。これらのほうが簡潔で,何がやりたいのか明瞭です。
ループ変数(n
のこと)をあらわに初期化する記述もインクリメントする記述も不要です。
最初に掲げたコードについて,「実際的なコードではなくても,while
の働きを簡潔に説明するのが目的だから問題ない」という反論があるかもしれません。
一理あると思います。ただ,初心者が現場でこういうコードを書いてしまわないよう,一言あってよいでしょう。
Ruby では while の出番は少ない
Ruby では while
の出番は少ない,と言い切ってよいと思います。
matz(まともとゆきひろさん)が,「メソッドとブロックという仕組みで繰り返し処理が簡潔に書ける」ことを発見したことで Ruby の大きな特長が生まれたと思います4。いや知らんけど。
値をすこしずつ変えてループを回すような場合,たいがいそれに適したメソッドがあります。times
,upto
,downto
,step
などですね。
コレクション5の要素について繰り返すメソッドも非常に充実しています。これらは Enumerable モジュールに集約されています。
Enumerable の機能を幅広く使えるようにするための Enumerator という仕組みもあります。
これら繰り返しのためのメソッドをイテレーターと呼んでいます6。
以下に Ruby の繰り返し処理の例を挙げます。
# 双子素数(差が 2 である素数ペア)の最初の 10 組を表示
require "prime"
p Prime.lazy.each_cons(2).select{ _1 + 2 == _2 }.take(10).to_a
# => [[3, 5], [5, 7], [11, 13], [17, 19], [29, 31], [41, 43], [59, 61], [71, 73], [101, 103], [107, 109]]
# 文字列の偶数番目の文字を取り出して繋げる
puts "clump".chars.select.with_index{ _2.even? }.join
# => "cup"
while
で書こうとするとなかなか面倒ではないでしょうか。
while の使い道
ではどんな場合に while
の出番が回ってくるのでしょうか。
ループ変数が存在するようなループの場合,「ループ変数がどんなふうに変化していってどこで終了するか」が簡単に分かるようなループはたいがいイテレーターメソッドで足りそうです。
ループ変数が存在していて,while
が使われるのは,値の変化が簡単には言えない場合が多いのではないかと思います。
あるいはループ変数が存在しないようなループですね(具体例が思いつかず)。
コラッツの問題
while
がふさわしそうな例を一つ挙げます。
「コラッツの問題」とか「角谷の問題」と呼ばれる数学の未解決問題があります。
何か一つ自然数(つまり正の整数)を考えます。それが偶数なら 2 で割り,奇数なら 3 倍して 1 を足します。出来た数についても同じことを繰り返します。
そうすると,どんな自然数から始めても必ず 1 に達する,という予想があるのです。
しかしまだ証明はされていません。
証明されるか,逆に反例があることが示されれば解決ですが,この問題は超難問と言われているようです。
試しに 3 から出発して手計算でやってみましょう。
3 → 10 → 5 → 16 → 8 → 4 → 2 → 1
途中で 2 の累乗になれば一気に 1 にたどり着きます。
もっと長いステップを楽しみたければぜひ 27 からスタートしてみてください。
ともかく,人類がこれまでに調べた限りにおいてすべての自然数が最後には 1 にたどり着いたのだそうです。
では,上のような系列を表示するコードを書いてみましょう。
n = 3
while n > 1
print n, " → "
n = n.even? ? (n / 2) : (n * 3 + 1)
end
puts 1
しかし,これとてもイテレーターメソッドで書けます。
while
を一切使わないで(しかも無理をせずに)プログラミングすることは可能です。
もう一つくらい while
の使用例を挙げようと思いましたが,どうも気の利いたのが思いつきません。どなたか教えてください。
-
この順序は重要。
while
の論理反転としてuntil
がある。Ruby のuntil
はwhile
の真/偽を逆にしただけだが,Pascal のrepeat
〜until
は本体を実行したあとで条件式を評価して脱出するか否かを決める(なので最低 1 回は本体が実行される)。 ↩ -
プログラミングスクールの客寄せ記事や Qiita の記事など。 ↩
-
ここでは 1 から 9 までのループをどう書くかという観点でコードを示した。単に 1 から 9 まで改行しながら表示させたいなら
puts [*1..9]
でよい。 ↩ -
配列やハッシュなど要素の集まりであるようなオブジェクトをコレクションと呼ぶ。 ↩
-
一般的な用語としてはイテレーターはもっと広い概念。 ↩