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の処理はこうなります
- atに初期値(=0)を入れる
- atとalistの最初の要素をブロックに渡す
- ブロックの戻り値をatとして、次の要素と一緒にブロックに渡す
- 最後のブロックから戻ってきた値を返す
問題は、初期値を省略すると、最初の要素が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
- btに初期値(=0)を入れる
- 省略されたら最初の要素(=4)
- btのシンボルで指定されたメソッドを、次の要素を引数にして呼び出す
- つまり、4.+(5)
- ブロックの結果をbtとして、次の要素と一緒にブロックに渡す
- 最後のブロックから戻ってきた値を返す
これもコードで書いてしまいましょう
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} # 実際は動かない