Refinementsで遊んでみた

はじめに

Refinementsで遊んでみた記事です。具体的な内容としてはトップレベルでRefinementsが使えないかなと試してみたとき備忘録です。

Refinementsとは

Rubyでは、後から自由にクラスにメソッドを追加することができる。

class Integer
  def hoge
    puts :hoge
  end
end

42.hoge
# => :hoge

これはこれで便利なんだけど、グローバルに変更されるし、継承とかでも意図しない動きをする可能性があるのであまりいいとは言えない。

そこでRefinementsの出番。

moduel Refine
   refine Integer do
      def hoge
         p :hoge
      end
   end
end

# 42.hoge
# => Error!

using Refine

42.hoge
# => :hoge

Refinementsではusingした移行で再定義したものを使える。 またusingした箇所のファイルスコープに限定されるので影響範囲が小さくて済む。

やりたかったこと

以下のようにトップレベルでrefineを使って特定のクラスにメソッドを生やしたかった(毎回、usingとか書くのが面倒だったので……)

refine Integer do 
   def hoge
      puts :hoge 
   end
end

42.hoge

ただ、このコードはrefineというメソッドが定義されていないのでエラーになる。

やってみた

で、出来そうか試してみたところ以下のコードで動作した。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

refine Integer do 
   public
   def hoge
      p :hoge 
   end
end

42.hoge
# => :hoge

やっていること

Refinementsをよく使われる人は以下のようなコードで無名モジュールを使っているんじゃないかと思う。

using Module.new {
    refine Integer do
        def hoge
            p :hoge
        end
    end
}

42.hoge
# => :hoge

このコードを元にKernelモジュールにrefineメソッドを作成。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

refineメソッドは第一引数に再定義したいクラスなどを受け取り、メソッドの再定義などはブロック引数で渡します。

これにより以下のようにhogeメソッドを追加できます。

refine Integer do 
   public
   def hoge
      puts :hoge 
   end
end

42.hoge
# => :hoge

ただし……

ただ、このコードはusingしていないのに再定義した内容が使用できているんですよね……。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

# ここで usingを使っていないが作成したhogeメソッドが使えている……
refine Integer do 
   public
   def hoge
      p :hoge 
   end
end

42.hoge
# => :hoge

これはRefinementsの仕様通りの動きなのかがはっきりとしていないのでなぜ動いているのやら……。

それと今回のコードではpublicを追加しないとメソッドが呼び出されなかった。

module Kernel
    module_function
    def refine klass, &block
        Module.new do
           refine klass do
              yield
           end
        end
    end
end

refine Integer do 
   def hoge
      p :hoge 
   end
end

42.hoge
# => private method `hoge' called for 42:Integer (NoMethodError)

エラーからprivateメソッドとして定義されているらしいことがわかったのでpublicを付けてみたら動いたという……。 この辺も仕様通りの動きなんだろうか……?

おわりに

とりあえず、やりたいことはできた。 これが仕様通りの動きなのかは判断がつかないので、Refinementsの実装とか読んでみようかと思う。

参考

qiita.com

secret-garden.hatenablog.com

secret-garden.hatenablog.com