Rubyチュートリアル 〜英文小説の最頻出ワードを見つけよう!(その10)

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


いいえ!
問題が発生しました!
先のコードは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だ! - hp12c


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


あっ!
ちょっと問題を発見しました
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するかしないか分けています


次回に続く)