カレーの恩返し

おいしいのでオススメ。

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#sumArray#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を使った方が良いよねっていうお話でした。

 

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

これを読みながら勉強中です。