Enumerable#sumが便利だ

集計メソッドを作りたいと思った

集計メソッド

こんなクラスが3つあるイメージ。

class Hoge
  class << self
    def count
      1
    end
    
    def find
      [:hoge]
    end
  end
end

class Fuga
  class << self
    def count
      2
    end
    
    def find
      [:fuga]
    end
  end
end

class Wib
  class << self
    def count
      3
    end
    
    def find
      [:wib]
    end
  end
end


要は、Railsのモデルクラス(の擬似クラス)。
で、このモデルを集計したい。集計対象は引数で指定できて、modeが1の時はHogeのみを、それ以外の時はFugaとWibが集計対象になる。集計方法はブロックで渡す。
使い方はこんなイメージ。

aggregate(1){|klass| klass.count} #=> 1
aggregate(0){|klass| klass.count} #=> 5
aggregate(1){|klass| klass.find}  #=> [:hoge]
aggregate(0){|klass| klass.find}  #=> [:fuga,:wib]
#こう書くほうがいいかな
#aggregate(0,&:find)

Enumerable#sumを使う(sumはActiveSupportによるEnumerableモジュールの拡張)

これはsum使うのが便利だろうと思って実装。

def aggregate(mode,&b)
  klasses = get_klasses_with(mode)
  klasses.map{|klass| yield(klass)}.sum
end

def get_klasses_with(mode)
  case mode
  when 1
    [Hoge]
  else
    [Fuga,Wib]
  end
end


できたー。けど、この時自分の中で、sumの実装は、以下と同じかと思っていた。

module Enumerable
  def sum
    inject{|sum,elem| sum + elem}
  end
end


なので、さっきのaggreagateコードを見て、mapとsumやるより、injectでやった方がいいやと思ってリファクタリング

#このコードはバグあります
def aggregate(mode,&b)
  klasses = get_klasses_with(mode)
  klasses.inject{|result,klass| result += yield(klass)}
end


しかし、このコードはバグってました(ノд`)。findの結果も配列だから、+で行けると思っていたのですが、以下の通りエラー。
これは、injectは初期値を指定しない場合、コレクション(klasses)の先頭のオブジェクトを使うため。

`aggregate': undefined method `+' for Fuga:Class (NoMethodError)
from c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:32:in `inject'


回避方法として、初期値として0を与えるか[]を与えるかをifで分けようかと思ったのですが、汚いなーと思い、おもむろにsumのソースを見る。

  • Enumerable#sum
def sum(identity = 0, &block)
  return identity unless size > 0
  if block_given?
    map(&block).sum
  else
    inject { |sum, element| sum + element }
  end
end


げげっ。sumって単純なinjectやってるだけじゃないのか。(普通に考えればそんな低機能なわけないか・・・)
ブロック渡した際には、mapしてsumしてる。再帰か。ってことは、最初の実装に近い。最初の実装でもmapとsum使っているので、最初のはこう書けるはず。

klasses.sum{|klass| yield(klass)}


あ、結局こうか。

def aggregate(mode,&b)
  klasses = get_klasses_with(mode)
  klasses.sum(&b)
end


sum便利だなー。mapして自分呼ぶところがいいなー。単純なコードだけど勉強になった。