Rubyは手続き型の言語であるが、LispやSmalltalkの影響も受けているため関数型言語の側面も持つ。
Wikipediaのアクセス解析プログラムを例に、両者の違いを比べてみる。元の手続き型のコードは、いがいがさん作のRuby講義資料から。
アクセス解析のプログラムは以下の処理を実施している。
- CSV形式のログ(ページ名とアクセス数が記録されている)を読み込む
- jaで始まる行のみを対象にする
- ログの1行からページ名とアクセス数を抽出
- アクセス数が多い順に並び替え
- トップ20行を表示
手続き型
元のコードはこちら。処理を順番に書いているところが手続き型。CやJavaで書いても同じ書き方になりそう。
require "cgi"
filename = "20120301-000000-ja.txt"
file = File.open(filename, "r:UTF-8")
list = []
while text = file.gets
begin
next unless text =~ /^ja/
data = text.split
h = {:title => CGI.unescape(data[1]), :count => data[-2]}
list.push h
rescue Exception => e
#p e
end
end
file.close
# count順にソート
result = list.sort_by do |i|
i[:count].to_i
end
# トップ20表示
result.reverse.first(20).each do |i|
puts i
end
関数型
これを関数型っぽく書き換える。
- 条件にマッチしない行を対象外にする …
while text = file.gets
とnext unless
を#select
へ - データを抽出した結果を配列に格納する …
list.push
を#map
へ - それぞれの処理をメソッドチェーンで繋ぐ
書き換えたコードがこちら。元のファイルに対して1つずつフィルタを適用しているような書き方。SQLやjQueryにも似てる。それから、UNIXやLinuxでgrepやcutコマンドを使ったパイプ処理にも。
require 'uri'
require 'pp'
filename = ARGV[0]
def parse(line)
data = line.split
{ title: URI.decode(data[1]), count: data[2].to_i }
end
open(filename) do |f|
pp f
.select {|line| line =~ /^ja/ }
.map {|line| parse(line) }
.sort_by {|item| item[:count] }
.reverse
.first(20)
end
このように同じプログラムでも色々な書き方ができることが、Rubyの面白いところ。
どう書くのがよいか
コンピュータの処理に近いのが手続き型、処理を抽象化したのが関数型。慣れると関数型のほうが分かりやすく読める。ずっと手続き型で書いてきて、関数型っぽく書けることを知ると、ついつい関数型っぽく書きたくなる。
ところが、必ずしもそうではない。このWikipediaアクセス解析プログラムの処理時間を time コマンドで計測すると、ハッキリと違いがでる。 → 【追記】この段落の内容は嘘でした。追記まで読んでください。
手続き型風のプログラム
13.70s user 0.35s system 99% cpu 14.061 total
関数型風のプログラム
30.87s user 0.49s system 98% cpu 31.911 total
なぜか。元のプログラムは1つのループ内で #select
と #map
相当の処理をやっている。書き換え版はそれぞれの処理でループを回し、別々の配列に格納している分だけ遅くなる。なので結局は、目的に応じて最適な手段は変わってくる。性能だけでも、ソート時に降順に並び替えておいて #reverse
を使わないなど、最適化する余地はたくさんある。遅延評価をする Enumerable#lazy を使えば効率化されるのだろうか(まだ試していない)。
Rubyはいろいろな書き方ができて、奥が深いという話でした。
追記
元のプログラムと条件を揃えないと意味が無いとツッコミを受けたのでやり直し。URI.decodeではなくCGI.unescapeを使って、ppもputsに書き換えた。
require 'cgi'
filename = "20120301-000000-ja.txt"
def parse(line)
data = line.split
{ title: CGI.unescape(data[1]), count: data[2].to_i }
end
open(filename, "r:UTF-8") do |f|
f
.select {|line| line =~ /^ja/ }
.map {|line| parse(line) }
.sort_by {|item| item[:count] }
.reverse
.first(20)
.each {|item| puts item }
end
これで速度を測ると、元のプログラムと遜色ないという結果に。
ruby wiki.rb 13.99s user 0.42s system 99% cpu 14.424 total
手続き型と関数型の違いで遅かったのではなく、CGI.escape
の代わりにURI.decode
を使ったのが原因で遅かったというオチでした。とほほ。