each、each_with_object、inject、map

Ruby の each、each_with_object、inject、map は使いどころが微妙に違う。
それぞれ適切な状況で使い分けられれば、コードはより分かりやすくなる。

どんな状況でも each で書くことはできる。だから、each だけ使いこなせればいいという考え方はある点で正しい。そのような考え方の人にとってはeach で書くのがもっとも分かりやすいコードになるだろう。

しかし慣れてみると上記のメソッドを使い分けられる方が簡潔で分かりやすいコードになる。その理由はメッセージ性の違いだ。

each ですべてを書く場合は余計なコードを書く必要があり、その分、どうしても、本質的なコードが埋もれてしまう。余分なコードがないほど、本質的なコードが際立つ。メッセージが伝わりやすくなる。意味があるコードの比率を下げることは、中級プログラマへの道を開く鍵だ。

それでは本題に入ろう。

まずは簡単に [1, 2, 3] をそれぞれ二乗して [1, 4, 9] を得るコードを書く。

なお、each_with_object の利用には Ruby 1.9 系を利用するか、Rails の一部である ActiveSupport を利用する必要かある。

# each
result = 
[1, 2, 3].each do |i|
  result << i*i
end
result # 値を返すために必要

# each_with_object
[1, 2, 3].each_with_object  do |i, result|
  result << i*i
end

# inject
[1, 2, 3].inject [] do |result, i|
  result << i*i
end

# map
[1, 2, 3].map do |i|
  i*i
end

この場合は map がもっともコードが短く簡潔になることが分かる。each の場合は返り値が self であるため、生成した result を明示的に返す必要がある。

この場合は、 each_with_object と inject の間の違いは引数の順序以外に特に分からない。

では、次の例を見てみよう。
[%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)] から {"0xff0000" => "red", "0x00ff00" => "green", "0x0000ff" => "blue"} を得たい。

# each
hash = {}
[%w(0xff0000 red), %w(0x00ff00 green),
 %w(0x0000ff blue)].each do |key, value|
  hash[key] = value
end
hash

# each_with_object
[%w(0xff0000 red), %w(0x00ff00 green),
 %w(0x0000ff blue)].each_with_object({}) do |(key, value), hash|
  hash[key] = value
end

# inject
[%w(0xff0000 red), %w(0x00ff00 green),
 %w(0x0000ff blue)].inject({}) do |hash, (key, value)|
  hash[key] = value
  hash # <- 余分だが必要な1行
end

# map
# ハッシュを返り値とできないため、かけない。

each_with_object では必ず引数のオブジェクトが返り値となるため、返り値となるオブジェクトを明示的に最後に書く必要はない。しかしながら、 inject ではブロックで最後に評価した値が次回のブロックの引数となりさらには返り値となるため、明示的に hash を最後に書く必要がある。

map は配列を返り値とする場合にしか利用できないため、ハッシュを得たい場合は利用できない。

この場合は、each_with_object を使う例がもっとも簡潔である。それは、内部の処理で副作用をもたらす処理を行っており、その副作用を与えたい対象が返したい値だからだ。inject の処理では適切な値を返すために 1行余分に必要になってしまう。

次に整数の和を求める例を示す。

# each
sum = 0
[ 1, 2, 3].each do |i|
  sum += i
end
sum

# each_with_object
# 破壊的に変更可能なオブジェクトでないと適用不可
sum = [ 1, 2, 3].each_with_object(0) do |i, j|
  j += i
end
# p sum #=> 0

# inject その1
sum = [ 1, 2, 3].inject 0 do |subtotal, i|
  subtotal + i
end
# inject その2
sum = [ 1, 2, 3].inject 0, :+
# inject その3
sum = [ 1, 2, 3].inject :+

# map は配列を返り値とする場合にしか利用不可

整数の和を得たい場合は、inject が一番簡潔になる。この場合は each_with_object は意図どおり動作しない。

最後に、文字列の連結処理を考える。

# each
path = ""
%w(usr local ruby ruby1.9.1 bin).each do |dir|
  path << "/" + dir
end
path

# each_with_object
%w(usr local ruby ruby1.9.1 bin).each_with_object "" do |dir, path|
  path << "/" + dir   # path += "/" + dir は不可
end

# inject
%w(usr local ruby ruby1.9.1 bin).inject "" do |path, dir|
  path + "/" + dir
end

# map
# 今回の主な趣旨とは異なるが Array#join を使う解を示す。
%w(usr local ruby ruby1.9.1 bin).map do |dir|
  "/" + dir
end.join("")

この場合も inject が簡潔であるように感じられるが、それは主にeach_with_object がスペルが長いからだ。

その部分を差し引いて考えれば、同等だろう。each_with_object はブロック引数のオブジェクトを破壊的に変更し続ける場合に便利なメソッドなので、この場合 path += ではなく path << を利用する必要がある。inject はブロックの最後の評価結果を利用するため、path << や path += などとする必要はなく、単に + として良い。

map と join を組み合わせる例が、案外簡潔で分かりやすい点も見落とすべきでない。この例もありうる。

総じて each を使うと、返したいオブジェクトの初期化と、値を返すために2行長くなる。この2行は本質的な処理を行っている箇所ではないため、each_with_object や inject、map を利用すれば2行短くできる。
逆にいうと、すでに得られているオブジェクトに対する処理で、返り値などを利用する必要がとくにない場合は each を利用する方が分かりやすいコードになる

補足であるが、tap (Ruby 1.8.7 以降)というメソッドや、returning (ActiveSupport が提供)というメソッドがある。これらを利用すると、返り値の1行などをなくせる場合がある。

# tap
{}.tap do |hash|
  [%w(0xff0000 red), %w(0x00ff00 green),
   %w(0x0000ff blue)].each do |key, value|
    hash[key] = value
  end
end

# returning
returning ({}) do |hash|
  [%w(0xff0000 red), %w(0x00ff00 green),
   %w(0x0000ff blue)].each do |key, value|
    hash[key] = value
  end
end


蛇足だが、さらに上記は Hash[] メソッドを使うことできわめて簡潔に書ける。

a = [%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)]
Hash[*a.flatten]