33.Ruby|ループの種類・使い分け

処理が遅くてトランザクションが中断される問題が頻発していて、リファクタリングをする機会に恵まれた。 この機会にRubyのループ処理について不足している知識を補い整理しておきたい。

forよりもeach

forとeachの違い

まず大きな違いとして、eachはブロック内外でスコープが仕切られているのに対して、 forはブロック内外でスコープが変わらない。

forを原則使わない方が良い理由としては、下記。 for内部はeachにより実装されており(わざわざforを使う理由が薄く)、 新しいスコープが導入されない為、外側からもアクセスできてしまう(無闇にアクセスできない方が良い)。 techracho.bpsinc.jp

スコープを作らないグループ:forやwhile

for

for 変数 in オブジェクト do
  繰り返し処理
end
for i in [1, 2, 3] do  # forではdoは省略可能
  num = i
end

p num
  • forではループ変数(i)もスコープが変わらず、forの外でも参照できる。
for i in [1, 2, 3]
end

p i
  • 出力結果
3

while

a = [1, 2, 3]
i = 0

while i < a.size
  num = a[i]
  i += 1
end

p num

スコープを作るグループ:eachやloop

each

例えばeachだとスコープを作るので、下記のようにスコープ外から呼び出すとエラーとなる。

[1, 2, 3].each do |i|
  num i
end

p num
  • 出力結果
20201123_loop_test:2:in `block in <main>': undefined method `num' for main:Object (NoMethodError)

eachはブロック内外でスコープが仕切られているが、forはブロック内外でスコープが変わらない。

しかしeachは飽くまでも

eachよりもmap

下記に詳しく書いていくが、mapよりもeachの方が冗長な記述になりやすく、 またループの内外で余計に処理を追加しなくてはいけない。

eachとmapの違い

eachが「元の配列」・mapが「処理後の配列」を返す。

each

list = (1..5).to_a.freeze

double = []  # 事前に変数を定義する必要
list.each do |i|
  double << i * 2  # ループの中でdoubleを組み立てる必要
end

p "2倍したリスト:#{double}"

map

list = (1..5).to_a.freeze

mapped = list.map do |i|
  i * 2  # 実行したい処理だけで済む
end

p "2倍したリスト:#{mapped}"
  • 出力結果
"2倍したリスト:[2, 4, 6, 8, 10]"

techracho.bpsinc.jp

map処理を簡潔に書きたい場合

例えば下記のような処理があったとして。

user_names = users.map do |user|
  user.name
end

下記のように簡潔に書くこともできる。 この&(アンパサンド)は、そのメソッドをブロックとして展開することを意味する。

user_names = users.map(&:name)

なおmapメソッドは、Enumerableモジュールに実装されたメソッド。 docs.ruby-lang.org - なおEnumerableモジュールとは、集合を表すクラスに数え上げや検索などのメソッドを提供するパッケージのようなものを指す。

https://tech-camp.in/note/technology/4423/

loop

loopは無限ループを作るのに適している。 意図的にループを終了(break)させない限り、永久に処理を繰り返す。

loop ブロック
# 例)
loop do
  繰り返し処理
  終了条件によってループ中断( break )
end

eachよりもfind_each

find_eachは分割してレコードを取得して処理をしてくれるので、eachよりもメモリの消費量を少なく抑えられるケースがある。 デフォルトで1000件ずつの処理をする。

qiita.com

大量のデータを処理する際、DBから取得してきたデータを each のループで回してしまうと、 ループを回す直前にデータを全てメモリ上に展開してしまいます

取得するデータ量が多い場合は、 find_each を使ってDBからデータを分割して取得するようにし、 メモリ上に展開されるデータ数を絞る方がパフォーマンスが良い場合があります

ちなみに、 find_eachは in_batches で取得したデータをeachで返しています

× データ数が多いと一度に全てをメモリ上に展開してしまうためパフォーマンスが悪い

Log.all.each do |log|
  # 処理を実行
end

◯ DBから分割して取得してくれるので、必要とするメモリの量を抑えながら実行できる

Log.find_each do |log|
  # 処理を実行
end

each_slice

each_sliceは要素を複数ブロックに分けた上で繰り返し処理をさせたい場合に使う。

doc.okkez.net

n 要素ずつブロックに渡して繰り返します。 要素数が n で割り切れないときは、最後の回だけ要素数が減ります。 例)

(1..10).each_slice(3) {|a| p a}
    # => [1, 2, 3]
    #    [4, 5, 6]
    #    [7, 8, 9]
    #    [10]

pluck

qiita.com