集計メソッドを作りたいと思った
集計メソッド
こんなクラスが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モジュールの拡張)
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して自分呼ぶところがいいなー。単純なコードだけど勉強になった。