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するかしないか分けています
(次回に続く)