ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

Rubyっぽくリファクタリングするためのポイント(4)

今回の話は、「Rubyっぽい」ことは間違いないですが、
リファクタリングとして妥当かは微妙です


というのも、今回のコードを見たときに、
Rubyにあまり詳しくない人には、
何をやっているのか分からない可能性があるからです


保守性、という意味では微妙ではありますが、
使い慣れるとものすごく便利なので、紹介しておきます


<今回の仕様>

alist = [[:a, 1], [:b, 2], [:c, 3]]
blist = [4, 5, 6]
  • alist/blistに含まれる各数値の和を求める


あほみたいに易しい問題ですが、とりあえずべた書き

at = 0
alist.each {|a| at = at + a.last}
p at #=> 6

bt = 0
blist.each{|b| bt = bt + b}
p bt #=> 15

※ちょっと事情があって、意図的に自己代入(+=)を使ってません


eachブロックも使ってるし、a.lastなんて書き方はRubyっぽいと思いますが、
ここで「Enumerable#inject」というのを持ち込むと、
alistに関してはこんな風に書けます

p alist.inject(0){|at, a| at + a.last} #=> 6

唐突に見せられても何が何だかわからないと思いますが、
一つずつ解説していきます

inject(初期値){|前の結果, 次の要素| 結果と要素をどうにかする式(の値)}

つまり、今回のalist.injectの処理はこうなります

  1. atに初期値(=0)を入れる
  2. atとalistの最初の要素をブロックに渡す
  3. ブロックの戻り値をatとして、次の要素と一緒にブロックに渡す
  4. 最後のブロックから戻ってきた値を返す

問題は、初期値を省略すると、最初の要素がatに渡されることです

p alist.inject{|at, a| at + a.last} rescue (puts $!) #=> can't convert Fixnum into Array
p alist.inject{|at, a| at + a} #=> [:a, 1, :b, 2, :c, 3]

つまり、at=[:a, 1]というArrayになってしまうので、
Arrayと数値を足せないという例外が発生しますし、
aをそのまま足すと結合されていきます


正直、これだけ見てもわからないと思うので、
コードで説明しちゃった方がわかりやすいと思います

class InjectExample

  def initialize(list)
    @list = list
  end

  def inject(init = nil)
    list = @list.dup
    rsl = init
    rsl ||= list.shift

    list.each do |a|
      rsl = yield(rsl, a)
    end

    rsl
  end

end

iea = InjectExample.new(alist)
p iea.inject(0){|at, a| at + a.last} #=> 6

正確には違っているはずですが、大まかな動きはわかるはずです


続いてblistに関してですが、なんとたったこれだけです

p blist.inject(:+)     #=> 15

# 初期値を与えてもいい
p blist.inject(0, :+)  #=> 15
# 初期値を変えた場合
p blist.inject(10, :+) #=> 25 
  1. btに初期値(=0)を入れる
    • 省略されたら最初の要素(=4)
  2. btのシンボルで指定されたメソッドを、次の要素を引数にして呼び出す
    • つまり、4.+(5)
  3. ブロックの結果をbtとして、次の要素と一緒にブロックに渡す
  4. 最後のブロックから戻ってきた値を返す

これもコードで書いてしまいましょう

class InjectExample

  def initialize(list)
    @list = list
  end

  def inject(init = nil)
    list = @list.dup
    rsl = init
    rsl ||= list.shift

    list.each do |a|
      rsl = yield(rsl, a)
    end

    rsl
  end

  def inject_with_sym(sym)
    list = @list.dup
    rsl = list.shift

    list.each do |a|
      rsl = rsl.__send__(sym, a)
    end

    rsl
  end

end

ieb = InjectExample.new(blist)
p ieb.inject_with_sym(:+) #=> 15

さて、これだけ見ると、
「たかが合計を求めるためにこんな面倒なメソッドを持ち込まなくても」
と思うかもしれません


しかし、injectの本質は・・・

val = init
list.each do |data|
  # valとdataを処理
end
val

・・・こういったコードを抽象化することにあります


なので、こういうコードも書けます

hash = alist.inject({}){|h, a| h[a.first] = a.last; h}
# 最後にhを返さないとおかしくなるので注意(後述)
p hash #=> {:a=>1, :b=>2, :c=>3}

だんだんinjectの価値が見えてきたでしょうか(´・ω・)?



<おまけ>


最後のArrayをHashに変換するところで、
わざわざブロックの最後で「h」を書いています
これはなぜでしょう?


試しにhを消してみます

alist.inject({}){|h, a| h[a.first] = a.last} rescue (puts $!)
#=> undefined method `[]=' for 1:Fixnum

先ほどのInjectExampleを思い出してもらいたいのですが、
次のブロックに渡されるのは、前のブロックを評価した「値」です


なので、最後にhを値として返さないと、
次のブロックのhがおかしくなるわけです


最初にeachで書いたコードですが・・・

at = 0
alist.each {|a| at = at + a.last}
p at #=> 6

・・・このブロックの前の「at」が第一引数に移動したと考えるとわかりやすいかと

alist.each{|at, a| at + a.last} # 実際は動かない