refineは一体どこで真価を発揮するのか
結論
ナンセンスな標準メソッドの実装にパッチを当てたくなったときにrefineは真価を発揮する。
以下、結論に至るまでに道のりです。
refineとは
Rubyにはrefineというどんなクラスのメソッドでもローカルで再定義できる機能がある。
[1] pry(main)> module StringExtensions [1] pry(main)* refine String do [1] pry(main)* def reverse [1] pry(main)* "esrever" [1] pry(main)* end [1] pry(main)* end [1] pry(main)* end => #<refinement:String@StringExtensions> [2] pry(main)> [3] pry(main)> module StringStuff [3] pry(main)* using StringExtensions [3] pry(main)* "my_string".reverse [3] pry(main)* end => "esrever" [4] pry(main)> "my_string".reverse => "gnirts_ym"
refineを含んでいるStringExtensions
をusingで呼び出した場所からStringStuff
の終わりまで再定義が有効になる。
refineの構文についての細かい説明は省略するので上記のコードからふわっと理解してほしい。
Railsに潜る
refineを知った最初は「めっちゃ便利じゃん!」と思ったけど、よく考えてみると大抵のことはサブクラス作って親クラスのメソッドオーバーライドを行えば解決しそうだし、refineを使わないと解決できない問題が思いつかなかった。
なのでrefineの真価を発揮する場面をRailsのソースコードから調べてみた。
すると1ヶ所だけヒットした。
% find . -type f -name "*.rb" | xargs grep 'refine' -n ./activesupport/lib/active_support/core_ext/enumerable.rb:142: refine Array do
# https://github.com/rails/rails/blob/32431b37704c0aaec06ae1a23e0d6091d6542fd2/activesupport/lib/active_support/core_ext/enumerable.rb 一部抜粋 # Array#sum was added in Ruby 2.4 but it only works with Numeric elements. # # We tried shimming it to attempt the fast native method, rescue TypeError, # and fall back to the compatible implementation, but that's much slower than # just calling the compat method in the first place. if Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false) # Using Refinements here in order not to expose our internal method using Module.new { refine Array do alias :orig_sum :sum end } class Array def sum(init = nil, &block) #:nodoc: if init.is_a?(Numeric) || first.is_a?(Numeric) init ||= 0 orig_sum(init, &block) else super end end end end
Ruby 2.4でArray#sum
が追加されたけど要素にNumeric以外を入れるとTypeErrorになっちゃうからその辺イイ感じにしちゃうよー
って感じのコメントが書いてあるような気がする。
読んでいく。
if Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false)
Array#sum
が存在するかつ[‘a’].sum
が例外を吐くときTrue
using Module.new { refine Array do alias :orig_sum :sum end }
if文中はArray#sum
がArray#orig_sum
という別名を定義
class Array def sum(init = nil, &block) #:nodoc: if init.is_a?(Numeric) || first.is_a?(Numeric) init ||= 0 orig_sum(init, &block) else super end end end
要素がNumericのときはArray#orig_sum
(再定義前のArray#sum)を使い、それ以外のときはEnumerable#sum
でイイ感じにする
(今回の目的はArray#sumの解読ではないのでこれ以上の追跡は省略する)
再定義前のArray#sum
を使ってnativeに任せられるところは任せている。
さらにif文を抜けるとArray#orig_sum
は使えなくなるため無駄に汚染することもない。
でもrefineなんか使わなくても以下のように直接Arrayクラスでprivateなorig_sumを宣言してしまえばいいじゃないかと思う人がいるかもしれない。
class Array alias :orig_sum :sum private :orig_sum def sum(init = nil, &block) # ... end end
refineを使ったときと使わないときの差を考えてみる。
refineを使用したとき
irb(main):001:0> module ArrayExtension irb(main):002:1> refine Array do irb(main):003:2* alias :sumsum :sum irb(main):004:2> end irb(main):005:1> end => #<refinement:Array@ArrayExtension> irb(main):006:0> module Hoge irb(main):007:1> using ArrayExtension irb(main):008:1> [1,2,3].sumsum irb(main):010:1> end => 6 irb(main):011:0> [1,2,3].sumsum NoMethodError: undefined method 'sumsum' for [1, 2, 3]:Array irb(main):012:0> [1,2,3].send(:sumsum) NoMethodError: undefined method 'sumsum' for [1, 2, 3]:Array
usingで呼び出したmodule中のみでArray#sumsum
が使えるがmodule外ではsendメソッドを使っても呼び出せない。
refineを使用してないとき
irb(main):001:0> class Array irb(main):002:1> alias :sumsum :sum irb(main):003:1> private :sumsum irb(main):004:1> end => Array irb(main):005:0> [1,2,3].sumsum NoMethodError: private method 'sumsum' called for [1, 2, 3]:Array irb(main):006:0> [1,2,3].send(:sumsum) => 6
Array#sumsum
はprivateメソッドなので[1,2,3].sumsum
はNoMethodErrorを吐くが、sendメソッドを使った場合Array#sumsum
が呼び出せてしまう。
今回の目的は標準メソッドにパッチを当てることなのでパッチを当てる前のメソッドが呼び出せてしまうのは意に反している。
だから標準メソッドにパッチを当てたいときはrefineを使った方が良いよねっていうお話でした。
- 作者: Paolo Perrotta,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/10
- メディア: 大型本
- この商品を含むブログ (3件) を見る