手続き型からのパラダイムから抜けられない君へ
序章
JavaやCなんかの手続き型から入ると、Rubyやその他スクリプト言語の独特な書き方や新しいパラダイムなんかはなかなかとっつきにくい。
その中でも脱初心者的な要素でありながらも特にとっつきにくいHashクラスのeach, map, select, reduce
から始めてみよう。
まずは簡単なサンプルから
まずはrubyの簡単なサンプルから。おそらくいちばんとっつきやすいものは each 文だろう。
僕が手続き型のパラダイムから抜け出せないときには、for文やwhile文を使って以下のような処理を書きがちだった。
array = [1,3,5] while i < array.length print "#{array[i] * 3}\n" i += 1 end
これはひどい!ただ、当時の僕はrubyで、他言語で慣れ親しんだfor文を書きたかったのだ。しばらく経つと以下のような書き方に変わっていった。
array = [1,3,5] for val in array do print "#{val * 3}\n" end
当時としてはまあ満足していた書き方だった。Javaのfor-inループのような感じだし、書いていたときには特に無駄を感じていなかった。
each文の活用
しかし、他のRubyコードを見るにつけ、これをよりrubyらしく書きたい欲望がふつふつと湧き出てきた。というわけで、ステップバイステップで手続き型から抜け出してみよう。
array = [1,3,5] array.each do |val| print "#{val*3}\n" end
[1,2,3]
という配列に対し、それぞれ同様の演算を行なう。each文の使いどころははじめの一歩としては非常に扱いやすいのではないかと思う。
もう少し複雑に
以下のようなリストがあるとしよう。
mail_list = [ "yamada-taro@example.com", "suzuki-hanako@mail.jp", "takahashi-ichiro@example.uk", "matsumoto-yukihiro@ruby.com" ]
このリストから以下のようなハッシュを作成したい。
[ { family_name: "yamada", given_name: "taro", domain: "example.com" }, { family_name: "suzuki", given_name: "hanako", domain: "mail.jp" }, : ]
これを手続き型っぽく書くとなかなか煩雑になる。僕が書くとしたらこんな感じか。
result = [] for ml in mail_list do name = ml.split("@")[0].split("-") given_name = name[0] family_name = name[1] domain = ml.split("@")[1] result << {family_name: family_name, given_name: given_name, domain: domain} end
少し恣意的な気もするが、大方このような書き方になるだろう。
単純に各要素への計算結果を予め宣言しておいた配列(result)に順次格納するコードだ。これをeachを使った文法に書き換えてみる。
result = [] mail_list.each do |ml| name = ml.split("@")[0].split("-") given_name = name[0] family_name = name[1] domain = ml.split("@")[1] result << {family_name: family_name, given_name: given_name, domain: domain} end
中身は全く変わらない。for-in
ループが単純にeach
句に変わっただけだ。ただ、これだけでも手続き型のパラダイムから移ってきた人間には大した変化に見える。
mapの登場
ここで、mapに登場してもらおう。mapもeachと同じく、各要素に対して何かしらの処理をするというメソッドだ。上記の例をmapで書き直してみよう。
mail_list.map do |ml| name = ml.split("@")[0].split("-")[0] given_name = name[0] family_name = name[1] domain = ml.split("@")[1] {family_name: family_name, given_name: given_name, domain: domain} end
これでも中身はあまり変わっていないように見える。しかし、大きな変化はresult
がなくなったことだ。決して省略しているわけではない。each文では、スコープの問題を解決するため、result
を予めループの外で宣言してやる必要がある。しかし、map
では、それ自体が最終の評価値を返すので、スコープ外で変数を宣言してやる必要がないのだ。結果を取り出すには、以下のようにすればいい。
result = mail_list.map do |ml| : end
このmap
は結果をそのまま返すという事実に気づくと、これをメソッドチェーンにしてつなぐこともできるという事実にたどり着く。これこそがmapの強みの一つである。
ここで、メソッドチェーンについて復習しておこう。メソッドチェーンはあるメソッドの戻り値を直接用いる記法のことだ。以下に例を示す。
"good morning, sir".delete('. sir').capitalize #=> "Good morning"
ここでは、"good morning, sir".delete('. sir')
が"good morning"
を返すため、これにさらにcapitalize
をつなげ、結果を得ている。
先程の例に戻り、メソッドチェーンを用いて書き直してみよう。
mail_list.map {|ml| ml.split("@") }.map{|ml| {name: ml[0].split("-"),domain: ml[1]} }.map{|ml| {family_name: ml[:name][0], given_name: ml[:name][1], domain: ml[:domain]} }
かなり見た目が変わった。そして、可読性も上がったと思う。
括弧が初出なので、少し解説しよう。
{}の括弧はdo-end
と同じ意味だ。人によって使い分けが違うと思うが、僕はメソッドチェーンを使うときには{}括弧を使うようにしている。
例が単純で、少し冗長な書き方ではあるが、mapの威力を端的に表せていると思う。
また、関数型という味方をすれば、mail_list
自体の値は変わっていないことにも注目すべきである。この処理を何度処理しても同じ結果が得られるはずだ。
select
その他にも例を見ていこう。例えば以下のリスト(再掲)から .com で終わるメールアドレスを抜き出してみる。
mail_list = [ "yamada-taro@example.com", "suzuki-hanako@mail.jp", "takahashi-ichiro@example.uk", "matsumoto-yukihiro@ruby.com" ]
selectでは、ブロック({}で構成されるもの)の中が真であるものだけを抽出する。
mail_list.select {|ml| ml.include?(".com") } # => ["yamada-taro@example.com", "matsumoto-yukihiro@ruby.com"]
そして、ここから先程の例である、 family_name
と given_name ,domain
への分割処理につなぐことができる。
mail_list.select {|ml| ml.include?(".com") }.map {|ml| ml.split("@") }.map{|ml| {name: ml[0].split("-"),domain: ml[1]} }.map{|ml| {family_name: ml[:name][0], given_name: ml[:name][1], domain: ml[:domain]} } # => => [{:family_name=>"yamada", :given_name=>"taro", :domain=>"example.com"}, {:family_name=>"matsumoto", :given_name=>"yukihiro", :domain=>"ruby.com"}]
reduce
reduce
はこれら Hash のメソッドの中でも取り扱いが少し難しい。そのため、詳しい説明ははぶく。以下の例では、ドメインが .com で終わるメールアドレスの数を出している。
mail_list.reduce(0) {|sum,ml| ml.include?(".com") ? sum + 1 : sum } #=2
ml
はこれまで同様、 mail_list
の要素を返す。 sum
は一度目は引数の値、それ以降は前回の結果を返す。そのため、合計値等を算出するのによくつかわれる。