Araoの技術ブログ

見習いエンジニアのAraoが、学んだことなどを書いたりするブログです。

Rubyの研修その2 - mapとinjectを使ってみた

Ruby勉強中の僕が、mapとinjectを使ってみたり、Rubyっぽい書き方に挑戦してみた話です。
この記事で使っているRubyのバージョンは2.1.4です。

例によって、研修の課題を受け取りました。

偉い人「タブ区切りテキスト、meibo.txtがあります。名前、性別、年齢の順に並んでいます。年齢の平均を標準出力に出力してください」

meibo.txt

john    m   18
paul    m   20
alice   f   15
dabid   m   17
jasmin  f   17

meibo.txtを見た僕「(Rubyでのファイルオープンの方法は知らないから調べるとして、二次元配列のようなものに格納してあげれば、あとはどうにでもできるはずだ)」
というわけで、ファイルオープンやgets、splitについて調べ、最初にこんなコードを書きました。

avg1.rb

f = open("meibo.txt")
index = 0
data = []
while line = f.gets do
    data[index] = line.split
    index += 1
end
f.close

sum = 0
data.each do |person|
     sum += person[2].to_f
end
avg = (sum / data.length).round(2)
puts avg

まず、ファイルをオープンし、一行ずつ読み込んだものを空白文字で分割して得られた配列を、配列dataの要素として順に代入します。次に、配列dataの各要素(配列)の年齢の部分を順次足していって、年齢の合計を求めます。最後に、年齢の合計を配列dataの要素数(人数)で割って、年齢の平均を求めて四捨五入で少数第二位までに丸め、標準出力に出力します。
我ながらよく書けているんじゃないかと意気揚々と偉い人に提出したのですが、返ってきた答えは(そのときの僕にとっては)意外なものでした。

偉い人「じゃあRubyっぽくmapとかinjectとか使ってみようか。これはこれでお行儀よく書けているけど」

Rubyにはmapやinjectというメソッドがあって、これらを使えばもっとRubyっぽく、シンプルに(?)書けるということなのでしょうか。そんなわけで、mapとinjectについて調べてみます。

参考:Rubyリファレンス map, map! (Array)Rubyリファレンス inject (Enumerable)

なるほど。mapは配列の各要素に関してブロックを実行し、そのブロックの戻り値を集めた配列を返すメソッドで、injectは繰り返し計算に計算に用いるメソッドということらしいです。
injectの方は最初ちょっとよくわからなかったんですが、計算を始める前に初期値が0のsumのような変数を用意する必要がない(injectしてあげるといきなり合計値がババーンと得られる)と理解しました。

mapとinjectがどんなものかわかったところで、さっそく先ほど書いたavg1.rbを書き直してみました。

avg2.rb

ages = STDIN.map do |line|
    line.split[2].to_f
end

sum = ages.inject do |sum, num|
    sum + num
end

puts (sum / ages.length).round(2)

さっきと比べてだいぶすっきりしました。先ほどのdataのような配列はなく、変数sumもinjectの戻り値をデデーンと代入しているだけです。
mapは配列だけでなく、複数行の標準入力に対しても同じようなことができるみたいですね。
ちなみに、さっきと違って標準入力からmeibo.txtを読み込んでいるので、実行するときはこのようにしてあげる必要があります。

$ ruby avg2.rb < meibo.txt

さて、ここから先は少し余談になってしまうのですが、偉い人からのコメントを受け、よりRubyっぽい書き方にも挑戦してみました。
もし今回の問題が、年齢の平均を求めるのではなく、年齢の合計を求めるものだったらどのように書けるでしょうか。

現時点の僕がめいっぱいRubyっぽくしてみると、こんな感じになります。

sum.rb

puts STDIN.map{|line|
    line.split[2].to_i
}.inject{|sum, num|
    sum + num
}

ぱっと見なんのことだかわからないかと思いますが、やっていることは先ほどとほとんど同じです。ただし今回は、人数(要素数)で割るという操作が必要ないため、こんなにシンプルになりました。
また、先ほどまではeachやmap、injectを使う際にdoとendで書いていたのを、代わりに"{ }"で書いています。こうすることで、mapが返した配列についてさらにinjectしてあげる、ということが可能になっています。

こういうのを書けるようになると、Rubyに慣れてきたなと実感しますね。