娯楽開発

娯楽開発

開発は娯楽。遊び心で開発をすすめるブログ。

【ruby】ループ処理いろいろ

最近rubyのコードを触る中でいろんなループ処理を捌く機会があったので備忘録として書き留めておきます。

今回の記事で整理しているループ処理のメソッドは

  • each
  • map
  • inject
  • each_with_object

の4つです。

ループ処理は決してこれだけではないので、用途に合わない場合はいろいろ調べてみてください。

それでは早速見ていきます。

eachmap

eachmapで同じコードを書くと以下のような違いが出ます。

pry(main)> [1, 2, 3, 4, 5].each do |elm|
pry(main)*   elm * 2
pry(main)* end
=> [1, 2, 3, 4, 5]
pry(main)> [1, 2, 3, 4, 5].map do |elm|
pry(main)*   elm * 2
pry(main)* end
=> [2, 4, 6, 8, 10]

eachでは、ループ処理の中身に関係なく受け取った値をそのまま返します。

一方mapでは、ループ処理の中の最後の評価値を配列の要素に代入した値を返します。

mapの場合「最後の評価値」ということですが、例えば処理の最後にputsを使用していた場合は以下のようにnilが入ることになります。(putsは値を表示後nilを返すため。)

pry(main)> [1, 2, 3, 4, 5].map do |elm|
pry(main)*   puts (elm * 2)
pry(main)* end
2
4
6
8
10
=> [nil, nil, nil, nil, nil]

これもeachであれば最後の処理に関係なく受け取った配列と同じ配列が返ってきます。

pry(main)> [1, 2, 3, 4, 5].each do |elm|
pry(main)*   puts (elm * 2)
pry(main)* end
2
4
6
8
10
=> [1, 2, 3, 4, 5]

まとめると、

  • each → 与えたデータを単に参照したいだけの時(データに変更を加える必要のない時)
  • map → 与えたデータにをもとに何らかの新しいデータを取得したい時

のように使い分けると便利です。

injecteach_with_object

inject

injectでは複数の変数を与えることで簡単に配列の計算などを行うことができます。

まずeachを使った例を見てみます。

pry(main)> sum = 0
=> 0
pry(main)> [1,2,3,4,5].each do |n|
pry(main)*   sum = sum + n
pry(main)* end
=> [1, 2, 3, 4, 5]
pry(main)> sum
=> 15

eachを使うとループの外でsumを初期化するなど長くなってしまう計算も、injectを使うことでシンプルにループの中で完結させることができます。

pry(main)> [1,2,3,4,5].inject do |sum, n|
pry(main)*   sum + n
pry(main)* end
=> 15

さてこのinjectの繰り返し処理ですが、パイプ文字sumnはそれぞれ以下のように値を受け取っています。

pry(main)> [1,2,3,4,5].inject do |sum, n|
pry(main)*   puts "sum = #{sum}"
pry(main)*   puts "n   = #{n}"
pry(main)*   sum + n
pry(main)* end
sum = 1
n   = 2
sum = 3
n   = 3
sum = 6
n   = 4
sum = 10
n   = 5
=> 15

1回目のループではsumが初期値として配列の最初の値(1)を受け取り、nは二つ目の値(2)を受け取ります。そして2回目以降のループになるとsumは前回のループの最後の評価値(sum + n)を受け取り、nは順次配列の3つ目以降の値を受け取っていくという流れになります。

さらに、inject(初期値)とすることで以下のようにsumの初期値を指定することもできます。

pry(main)> [1,2,3,4,5].inject(0) do |sum, n|
pry(main)*   puts "sum = #{sum}"
pry(main)*   puts "n     = #{n}"
pry(main)*   sum + n
pry(main)* end
sum = 0
n   = 1
sum = 1
n   = 2
sum = 3
n   = 3
sum = 6
n   = 4
sum = 10
n   = 5
=> 15

この場合、nは1回目のループで配列の一つ目の値を受け取ることになります。

またinjectの引数にハッシュや配列を受け取ることも可能です。

pry(main)> [1,2,3,4,5].inject([]) do |array, n|
pry(main)*   array.push(n)
pry(main)* end
=> [1, 2, 3, 4, 5]

しかしmap同様2回目以降のループでは前回のループの最後の評価値が参照されるので使い方に注意が必要です。

each_with_object

ループのなかで任意のオブジェクトを作成したい時はeach_with_objectが便利です。

people = [
  { id: 1, name: "tanaka", age: 21, home_town:   "Tokyo"},
  { id: 2, name: "suzuki", age: 30, home_town:   "Osaka"},
  { id: 3, name:   "sato", age: 26, home_town: "Fukuoka"}
]

例として上のような人物情報から人の名前と年齢だけを抜き出したリストage_listを作りたいときを考えてみます。

mapだと、

pry(main)> age_list = {}
=> {}
pry(main)> people.map do |person|
pry(main)*   age_list[person[:name]] = person[:age]
pry(main)* end
=> [21, 30, 26]
pry(main)> age_list
=> {"tanaka"=>21, "suzuki"=>30, "sato"=>26}

となりループの外でハッシュの初期化や参照をしなければなりません。

これをinjectで扱うと以下のようにmapより簡単に書けます。

pry(main)> age_list = people.inject({}) do |hash, person|
pry(main)*   hash[person[:name]] = person[:age]
pry(main)*   hash
pry(main)* end
=> {"tanaka"=>21, "suzuki"=>30, "sato"=>26}

しかしinjectの2回目以降のループで変数hashはループの中の最後の評価値を受け取るためループの最後にhashを与えなければなりません。

ここでeach_with_objectではループ処理とオブジェクトの初期化を同時に可能であるため、

pry(main)> age_list = people.each_with_object({}) do |person, hash|
pry(main)*   hash[person[:name]] = person[:age]
pry(main)* end
=> {"tanaka"=>21, "suzuki"=>30, "sato"=>26}

となりmapinjectより簡潔に書くことができます。

injecteach_with_object似てるところが多いですが、以下のような違いがあります。

それぞれ

hoge.inject(object) do |one, two|

hoge.each_with_object(object) do |one, two|

とした場合、

  • injectは一つ目のパイプ文字(one)で、each_with_objectは二つ目のパイプ文字(two)でそれぞれ引数(object)を参照する
  • inject → 1周目:one=object, 2周目以降:one=前回の最終評価値
  • each_with_object → 1周目:two=object, 2周目以降:two=two

となります。

まとめ

使い分けがややこしいループ系メソッドの違いが理解できたでしょうか。

今回は備忘録記事になりましたが、少しでもメソッド理解の役に立てていれば幸いです。

(間違いや誤りがあればご指摘お願いします!)