なかなかいいですね。さてこれでもう完成でしょうか…

いいえ!問題が発生しました!先のコードはDRY原則に反します!!

DRY原則

「DON’T REPEAT YOURSELF!」1

達人プログラマは言いました。

「お前は二人も要らないよ!」

あるいは、

「愚鈍なる君の二度手間はダメ!」

そうです、同じコードの繰り返しは罪なのです!

もう一度コードを見てみましょう。

class Hash
    def top_by_value(nth)
     select { |key, val| yield val }.take_by(nth) { |key, val| -val }
    end
 
    def bottom_by_value(nth)
     select { |key, val| yield val }.take_by(nth) { |key, val| val }
    end
 end

「-」記号1つの差はありますが…確かに…そっくりです。Yes, I repeat myself…

Version10

Hashクラスにtake_by_valueというメソッドを作って、上のコードを1ヶ所に集約します。

class Hash
   def top_by_value(nth, &blk)
     take_by_value(nth, lambda { |v| -v }, &blk)
   end
 
   def bottom_by_value(nth,&blk)
     take_by_value(nth, lambda { |v| v }, &blk)
   end
 
   private
   def take_by_value(nth, sort_opt)
     select { |key, val| yield val }.take_by(nth) { |key, val| sort_opt[val] }
   end
 end

差し当たりtake_by_valueはクラスの中から呼ぶだけなので、その可視性をprivateにします。

take_by_valueはtop_by_valueおよびbottom_by_valueに渡される引数nthの他、手続きオブジェクトsort_optを引数に取ります。top_by_valueおよびbottom_by_value側では、{|v| -v}または{|v| v}をlambdaでオブジェクト化して渡します。take_by_valueのsort_opt[val]は受け取った手続きオブジェクトを実行します。これはsort_opt.call(val)でもいいです。

またtop_by_valueおよびbottom_by_valueでは、受け取るブロックをtake_by_valueに引き渡すために、&blkでブロックを一旦オブジェクト化する必要があります。

ちょっと込み入った話になりました。関連する話題はここにも書いているので、参考になるかもしれません。

Rubyのブロックはメソッドに対するメソッドのMix-inだ!

兎に角、これでもう達人は怒らないでしょうか。

あっ!ちょっと問題を発見しました。top_by_valueにブロックを与えないで渡すとエラーがでます。

WORDS = ARGF.read.downcase.scan(/[a-z]+/)
 DICTIONARY = WORDS.inject(Hash.new(0)) { |dic, word| dic[word] += 1 ; dic }
 p DICTIONARY.top_by_value(30)
$ ruby topwords.rb 11.txt 1342.txt 18503.txt 
 topwords.rb:109:in `block in take_by_value': no block given (yield)

ブロックを渡さない場合は、範囲を限定しない結果を返すべきです。

Version11

ブロックがあるか無いかを判定するメソッドとしてblock_given?があります。それを使ってブロックがない場合、代わりのブロックをtake_by_valueに渡してあげます。

class Hash
   def top_by_value(nth, &blk)
     blk = lambda { |v| v } unless block_given?
     take_by_value(nth, lambda { |v| -v }, &blk)
   end
   
   def bottom_by_value(nth, &blk)
     blk = lambda { |v| v } unless block_given?
     take_by_value(nth, lambda { |v| v }, &blk)
   end
   
   private
   def take_by_value(nth, sort_opt)
     select { |key, val| yield val }.take_by(nth) { |key, val| sort_opt[val] }
   end
 end

上の例ではblock_given?の代わりに引数blkを使ってもいいです。これで問題はなくなりました。

と こ ろ が!

また恐ろしいことが起こりました。Hashクラスを見てください。

class Hash
   def top_by_value(nth, &blk)
     blk = lambda { |v| v } unless block_given?
     take_by_value(nth, lambda { |v| -v }, &blk)
   end
   
   def bottom_by_value(nth,&blk)
     blk = lambda { |v| v } unless block_given?
     take_by_value(nth, lambda { |v| v }, &blk)
   end
   
   private
   def take_by_value(nth, sort_opt)
     select { |key, val| yield val }.take_by(nth) { |key, val| sort_opt[val] }
   end
 end

lambda {|v| v }が4回も! Don’t Repeat Yourself! Yes, I repeat myself!!

達人…大至急直しますから…

Version12

lambda {|v| v }という手続きを返すoptというメソッドを定義しましょう。

class Hash
   def top_by_value(nth, &blk)
     blk = opt unless blk
     take_by_value(nth, opt(false), &blk)
   end
 
   def bottom_by_value(nth,&blk)
     blk = opt unless blk    
     take_by_value(nth, opt, &blk)
   end
 
   private
   def take_by_value(nth, sort_opt)
     select { |key, val| yield val }.take_by(nth) { |key, val| sort_opt[val] }
   end
 
   def opt(flag=true)
     lambda { |v| flag ? v : -v }
   end
 end

optメソッドのflag引数にデフォルトでtrueをセットしておきます。そうすればvalueがマイナスのときだけfalseを渡せばいいのです。

果たしてこれでコードが読みやすくなったのか、わたしにはわかりません。これはちょっとやり過ぎかもしれませんが、達人に怒られないということがここでは重要なのです。

と…ここまでやってミスに気が付きました。Version11のところでblock_given?の判定を、top_by_valueとbottom_by_valueのところでしました。でもこれをtake_by_valueのところでやればいいんです。そうすれば上のような手間は不要です。Version12はこんなやり方もあるんだという、ご理解でお願いします…

Version13

class Hash
   def top_by_value(nth, &blk)
     take_by_value(nth, lambda { |v| -v }, &blk)
   end
   def bottom_by_value(nth,&blk)
     take_by_value(nth, lambda { |v| v }, &blk)
   end
   private
   def take_by_value(nth, sort_opt)
     select { |key, val| block_given? ? yield(val) : val }.take_by(nth) { |key, val| sort_opt[val] }
   end
 end

take_by_value内で条件演算子(条件 ? 真 : 偽)を使って、ブロックの有無でyieldするかしないか分けています。

次回に続く)

  1. [Don't repeat yourself - Wikipedia](http://ja.wikipedia.org/wiki/Don't_repeat_yourself


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS