Re: 大量のハッシュデータを簡潔に作成する

これもどこまでマジなのかよくわからんのだけど……

つ inject

とだけ言わせていただく。

jijixi's diary - Re: Python での組み込み型をより自然な名前にする - kwatchの日記 , Re: 大量のハッシュデータを簡潔に作成する - kwatchの日記

inject()があればHash.create_with()はいらないというご指摘をいただきました。

検証してみましょう。

## Hash.create_with()を使う方法
data = Hash.create_with(:name, :gender, :role) {[
  ["Haruhi", 1, "Leader of SOS Brigade"],
  ["Mikuru", 1, "Time Traveler"],
  ["Yuki",   1, "Humanoid Interface"],
  ["Itsuki", 0, "ESPer Boy"],
  ["Kyon",   0, "Story Teller"],
]}

## inject()を使う方法
data = [
  ["Haruhi", 1, "Leader of SOS Brigade"],
  ["Mikuru", 1, "Time Traveler"],
  ["Yuki",   1, "Humanoid Interface"],
  ["Itsuki", 0, "ESPer Boy"],
  ["Kyon",   0, "Story Teller"],
].inject([]) {|rows, row|
  rows << {:name=>row[0], :gender=>row[1], :role=>row[2]}
  rows  ## 「rows<<x」の結果は「rows」なのでこの行は省略可能
}

・・・これならキー名が先に現れるHash.create_with()のほうがずっとわかりやすいや。

つうか、inject()すらいらなくね?collect()で十分やん。

## collect()バージョン
data = [
  ["Haruhi", 1, "Leader of SOS Brigade"],
  ["Mikuru", 1, "Time Traveler"],
  ["Yuki",   1, "Humanoid Interface"],
  ["Itsuki", 0, "ESPer Boy"],
  ["Kyon",   0, "Story Teller"],
].collect {|row| {:name=>row[0], :gender=>row[1], :role=>row[2]} }

これも悪くないな。でもまだHash.create_with()のほうがわかりやすいかな。

Hash.create_with()がわかりやすいのは、キーがはじめにまとまった形で指定できるから。それに対し、inject()もcollect()もキーがブロックのなかに散らかってしまう。わかりやすさという点では、この違いは大きい。


ベンチマークをしてみましょう。

class Hash
  def self.create_with(*keys)
    return yield.collect {|row|
      hash = self.new
      keys.zip(row) {|k, v| hash[k] = v }
      hash
    }
  end
end

n = ($N || 10000).to_i
lst = (1..n).collect {
  [rand(), rand(), rand(), rand(), rand()]
}

require 'benchmark'
Benchmark.bmbm(30) do |x|
  data1 = data2 = data3 = nil
  GC.start
  x.report('Hash.create_with') do
    data1 = Hash.create_with(:a, :b, :c, :d, :e) { lst }
  end
  GC.start
  x.report('Enumelable#inject') do
    data2 = lst.inject([]) {|rows, row|
      rows << {:a=>row[0], :b=>row[1], :c=>row[2], :d=>row[3], :e=>row[4]}
      #rows
    }
  end
  GC.start
  x.report('Enumelable#collect') do
    data3 = lst.collect {|row|
      {:a=>row[0], :b=>row[1], :c=>row[2], :d=>row[3], :e=>row[4]}
    }
  end
  $stderr.puts "** error: data1 != data2" if data1 != data2
  $stderr.puts "** error: data1 != data3" if data1 != data3
end

結果:

### ruby 1.8.7-p174
$ ruby -s hoge.rb -N=100000
Rehearsal -----------------------------------------------------------------
Hash.create_with                0.690000   0.020000   0.710000 (  0.728824)
Enumelable#inject               0.460000   0.020000   0.480000 (  0.489351)
Enumelable#collect              0.460000   0.020000   0.480000 (  0.485701)
-------------------------------------------------------- total: 1.670000sec

                                    user     system      total        real
Hash.create_with                0.880000   0.020000   0.900000 (  0.923916)
Enumelable#inject               0.630000   0.010000   0.640000 (  0.702285)
Enumelable#collect              0.380000   0.000000   0.380000 (  0.386911)
### ruby 1.9.1-p129
$ ruby -s hoge.rb -N=100000
Rehearsal -----------------------------------------------------------------
Hash.create_with                0.540000   0.050000   0.590000 (  0.588904)
Enumelable#inject               0.330000   0.030000   0.360000 (  0.363585)
Enumelable#collect              0.430000   0.020000   0.450000 (  0.471614)
-------------------------------------------------------- total: 1.400000sec

                                    user     system      total        real
Hash.create_with                0.670000   0.040000   0.710000 (  1.121027)
Enumelable#inject               0.400000   0.000000   0.400000 (  0.415909)
Enumelable#collect              0.390000   0.010000   0.400000 (  0.405874)

collect() 速え〜! そして Hash.create_with() 遅い〜!
10万件のデータでコンマ数秒の違いしかないけど、それでも速さを求める場合はHash.create_with()はやめたほうがいいですな。

多少タイプ量は多くなるだろうけど、はるかに柔軟。

別に今の話では柔軟性を求めちゃいなんだけどね。求めているのはわかりやすさと簡潔さだから、そこが劣っていたらいくら柔軟だからといっても訴求力ないっす。わかりやすさや簡潔さより柔軟性が必要になったら初めてその方法を採用すればいいだけのこと。ただそれだけ。