Meblog

このブログ記事は個人の見解であり、所属する組織の公式見解ではありません

手続き型からのパラダイムから抜けられない君へ

序章

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_namegiven_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 は一度目は引数の値、それ以降は前回の結果を返す。そのため、合計値等を算出するのによくつかわれる。

結び

以上、長々と説明した。

ステップ・バイ・ステップな説明を意識したため、冗長ではあるが、理解のギャップがないように構成したつもりだ。

僕はJavaからrubyを勉強したときになかなかこの文法になれなかった。しかも、これらを避けて手続き型で書いても当然同じ結果が得られる。

しかし、せっかくRubyという素晴らしい道具を手に入れたのだから、新しいパラダイムをとりいれ、脳みそを活性化するべきである。

僕のプロジェクトではあまり使わないが、それでも細かいツールやワンライナーでは頻出するため、覚えておいて損はないと思う。