プログラミングでは、繰り返しとは、コードのブロックを何度も繰り返す処理のことです。
これは、whileループやuntilループといった仕組みを使うことで実現できます。
自分自身を呼び出す関数である再帰を使うことでもできます。
しかし、たいていの場合は、Array、Range、Stringのようなコレクションを繰り返し処理したいはずです。
CrystalのArrayクラスとRangeクラスはどちらもEnumerableモジュールをインクルードしており、このモジュールには要素を繰り返し処理するためのメソッドが多数用意されています。
一方、Stringクラスには、文字を繰り返し処理するための独自のメソッドがあります。
また、Crystalには他の言語にあるようなfor文がありません。
その代わりに、繰り返し処理に使えるメソッドがいくつか用意されています。
コレクションを繰り返し処理する最も一般的な方法は、eachメソッドを使うことです。このメソッドは、コレクションの各要素をブロックに渡します。
これはRangeを使えば簡単にできます。
たとえば、1から3まで繰り返したいときは、eachメソッドを使ってその範囲を繰り返し処理できます。
(1..3).each do |n|
puts n
end
# Output:
# 1
# 2
# 3
もっと簡単に、決まった回数だけ繰り返したいだけであれば、Intクラスにあるtimesメソッドを使えます。
3.times do |n|
puts n
end
# Output:
# 0
# 1
# 2
Stringの繰り返し処理Stringは文字の並びであり、Enumerableモジュールには含まれていません。そのため、文字を繰り返し処理するための独自のメソッドを持っています。
Stringを繰り返し処理する最も一般的な方法は、each_charメソッドを使うことです。このメソッドは、Stringの各文字をブロックに渡します。
each_charメソッドがブロックに渡すのは、StringオブジェクトではなくCharオブジェクトです。
str = "hello"
str.each_char { |char| puts char }
# Output:
# h
# e
# l
# l
# o
Stringを繰り返し処理するもう一つの方法は、each_lineメソッドを使うことです。
このメソッドは、ファイルを1行ずつ読み込むときによく使われます。
str = "hello\nworld"
str.each_line do |line|
puts line
end
# Output:
# hello
# world
Enumerableモジュールをインクルードしたオブジェクトの繰り返し処理Enumerableモジュールには、コレクションの要素を繰り返し処理するためのメソッドが多数用意されています。
Enumerableモジュールをインクルードしているコレクションには、Array、Range、Hash、Setなどがあります。後者については、後の概念で扱います。
Arrayを繰り返し処理する最も一般的な方法は、eachメソッドを使うことです。このメソッドは、Arrayの各要素をブロックに渡します。
arr = [5, 2, 3]
arr.each do |element|
puts element
end
# Output:
# 5
# 2
# 3
mapmapメソッドもArrayを繰り返し処理する方法の一つで、各要素にブロックを適用した結果を含む新しいArrayを返します。
eachメソッドに似ていますが、変換後の要素を持つ新しいArrayを返します。
変換は、コレクションの各要素に関数を適用したいときに非常に便利で、よく行われる操作です。
arr = [1, 2, 3]
new_arr = arr.map do |element|
element * 2
end
new_arr
# => [2, 4, 6]
ifのような制御フローを使えば、どの要素を変換するかを絞り込むこともできます。
arr = [1, 2, 3]
new_arr = arr.map do |element|
if element.odd?
element * 2
else
element
end
end
new_arr
# => [2, 2, 6]
繰り返し処理している要素のインデックスを知りたいことがあります。その場合は、each_with_indexメソッドやmap_with_indexメソッドを使います。
これらのメソッドは、各要素とそのインデックスをブロックに渡します。また、開始インデックスを指定するための省略可能な引数も受け取ります。
arr = [1, 2, 3]
arr.each_with_index(4) do |element, index|
puts "Element: #{element}, Index: #{index}"
end
# Output:
# Element: 1, Index: 4
# Element: 2, Index: 5
# Element: 3, Index: 6
なお、Stringオブジェクトにはeach_char_with_indexメソッドも使えます。
sumsumメソッドは、コレクション内のすべての要素の合計を返します。また、合計する前に要素を変換するための省略可能なブロックも受け取ります。
さらに、合計の初期値を指定するための省略可能な引数も受け取ります。
arr = [1, 2, 3]
arr.sum
# => 6
arr.sum(2) { |n| n * 2 }
# => 14
reducereduce(他の言語ではfoldとして知られています)は、結合する方法と初期値を与えると、コレクション内のすべての要素を結合するメソッドです。
reduceメソッドには、ブロックに渡されるアキュムレーターがあります。アキュムレーターは、前回の繰り返しの結果です。
これは再帰的な処理になり、コレクション内のすべての要素を結合します。
arr = [1, 2, 3]
arr.reduce(0) do |acc, n|
acc + n
end
# => 6
reduceはsumに似ているように見えるかもしれませんが、アキュムレーターの初期値と結合方法を指定できるため、reduceのほうが柔軟です。
reduceを使って、sumやcountなどのメソッドを実装できます。
カードゲームのSpellbound Steelでは、warrior、mage、rogueといったキャラクターカードか、fireball、ice storm、lightning boltといった呪文カードのどちらかが入ったデッキを使います。ゲームには2人から4人が参加し、よりパワーレベルの高いカードを出して他のプレイヤーを倒すことが目標です。
カードのパワーレベルを計算したり、並べ替えたり、比べたりするいろいろなメソッドを作って、ゲームで優位に立ちましょう。
カードがデッキに入っているかどうか、わからないことがあります。そこで、探している種類のカードがデッキのどこにあるのか、最初の1枚の位置を教えてくれるメソッドがあると便利です。カードがデッキにないときは、それもわかるようにしたいところです。
SpellboundSteel.find_card?というメソッドを実装します。このメソッドは、StringのカードのArrayと、探すStringのカードを入力として受け取ります。探しているカードと一致する、デッキの中で最初のカードのインデックスを返します。カードがデッキにない場合は、nilを返します。
cards = ["Warrior", "Mage", "Rogue", "Fireball", "Ice Storm", "Lightning Bolt", "Mage"]
SpellboundSteel.find_card?(cards, "Mage")
# => 1
カードを読みやすくするために、カード名の各単語の先頭の文字を大文字にしたいと考えます。そうすれば、カードをプレイするときにカード名がすぐに見分けられます。
SpellboundSteel.capitalize_cardsというメソッドを実装します。このメソッドは、StringのカードのArrayを入力として受け取り、各単語の先頭を大文字にしたカードを返します。
cards = ["warrior", "mage", "rogue", "fireball", "ice storm", "lightning bolt"]
SpellboundSteel.capitalize_cards(cards)
# => ["Warrior", "Mage", "Rogue", "Fireball", "Ice Storm", "Lightning Bolt"]
ゲームでは、すべてのカードのパワーレベルを計算することが大切です。各カードのパワーレベルは、次の表のとおりです。
| カードの種類 | パワーレベル |
|---|---|
| Warrior | 10 |
| Mage | 20 |
| Rogue | 30 |
| Fireball | 15 |
| Ice Storm | 25 |
| Lightning Bolt | 35 |
SpellboundSteel.calculate_power_levelというメソッドを実装します。このメソッドは、StringのカードのArrayを入力として受け取り、すべてのカードのパワーレベルの合計を返します。
cards = ["Warrior", "Mage", "Rogue", "Fireball", "Ice Storm", "Lightning Bolt"]
SpellboundSteel.calculate_power_level(cards)
# => 135
ゲームには、呪いのカードと呼ばれるものがあります。これは、プレイした本人しかデコードできない隠しカードです。カードをデコードするには、カード名の2文字おきに文字を取り除きます。すると、カードの名前がわかります。
SpellboundSteel.decode_cardというメソッドを実装します。このメソッドはStringのカードを入力として受け取り、デコードしたカードを返します。
SpellboundSteel.decode_card("Wbalrrrlimoqr")
# => "Warrior"