54
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyの手続き型と関数型の側面

Last updated at Posted at 2014-05-04

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.getsnext 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を使ったのが原因で遅かったというオチでした。とほほ。

54
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?