Tech Racho エンジニアの「?」を「!」に。
  • 開発

RubyのRefinement(翻訳: 公式ドキュメントより)

こんにちは、hachi8833です。

自分の理解のためも兼ねて、Ruby 2.1から正式に導入されたRefinementのドキュメントを翻訳しました。適宜強調などを行っていますのでご了承ください。訳語はほぼまったく定着していないので、英語のrefinementで表記します。

refinementの理解は#usingメソッドの動作とスコープの理解にかかっていると感じました。#usingを書いた位置から下でrefinementが効くというあたりは、(機能は違いますが)privateキーワードと少し似ているように思います。

当時は知りませんでしたが、refinementの導入はかなり大変だったようです。以下も合わせてどうぞ。

Refinement

Rubyはオープンクラスなので、既存のクラスを再定義したり機能を追加したりできます。この手法は「モンキーパッチ(monkey patch)」と呼ばれています。残念なことに、モンキーパッチによる変更のスコープはグローバルなので、モンキーパッチのあたったクラスのユーザーすべてに影響が生じます。こうしたモンキーパッチが原因で、プログラムで意図しない副作用が発生することがあります。

refinementはモンキーパッチがクラスの利用者に与える影響を軽減するために設計され、クラスの拡張をローカルにとどめるための手段を提供します。

以下はrefinementの基本的な利用法です。

class C
  def foo
    puts "C#fooです"
  end
end

module M
  refine C do
    def foo
      puts "MのC#fooです"
    end
  end
end

最初にCというクラスを定義し、次にModule#refineでCをrefinementするという順序になります。refinementで変更できるのはクラスのみであり、モジュールをrefinementすることはできません。つまり、Module#refineの引数に与えられるのはクラスだけです。

Module#refineによって無名のモジュールが作成されます。このモジュールにはクラス(ここではC)に対する変更やrefinementが含まれます。refineブロック内の#selfがその無名モジュールであり、Module#module_evalと似ています。

refinementを有効にするには#usingを使います。

using M

c = C.new

c.foo # "MのC#fooです"が出力される

スコープ

(#usingで)refinementを有効にできるスコープは次の3つです。

  • トップレベル
  • クラス内
  • モジュール内

メソッドのスコープ内ではrefinementを有効にできません。

refinementが有効なのは、現在のクラス定義またはモジュール定義が終わるまでの間です。
トップレベルの場合は、現在のファイルの最下部までとなります。

Kernel#evalに渡す文字列内でrefinementを有効にすることもできます。この場合、refinementはevalされる文字列内でのみ有効となります。

refinementはスコープ内でレキシカル(≒静的)に動作します。refinementは#using呼び出し後、そのスコープ内でのみ有効となります。#usingより上の部分のコードではrefinementは無効です。

制御がそのスコープの外に移ると、refinementは無効になります。つまり、refinementを含むファイルをrequireしたり読み込んだりしても、refinementの現在のスコープの外で定義されているメソッドを呼び出したりしても、スコープ外でrefinementが有効になることはありません

class C
end

module M
  refine C do
    def foo
      puts "MのC#fooです"
    end
  end
end

def call_foo(x)
  x.foo
end

using M     # ここより上のコードにはrefinementが効かない

x = C.new
x.foo       # "MのC#fooです"が出力される
call_foo(x) #=> raises NoMethodError

refinementが有効なスコープ内でメソッドが定義されている場合、そのメソッドへの呼び出しは有効になります。次の4つのファイルに分かれたコード例をご覧ください。

  • c.rb:
class C
end
  • m.rb:
require "c"

module M
  refine C do
    def foo
      puts "MのC#fooです"
    end
  end
end
  • m_user.rb:
require "m"

using M   # ここから下はrefinementが効く

class MUser
  def call_foo(x)
    x.foo
  end
end
  • main.rb:
require "m_user"

x = C.new
m_user = MUser.new
m_user.call_foo(x) # "MのC#fooです"が出力される
x.foo              #=> raises NoMethodError

上のコードでは、MUser#call_fooが定義されているm_user.rbファイル内でMのrefinementが有効になっているので、main.rbでの#call_foo呼び出しも有効になります。

#usingはメソッドであり、このメソッドが呼び出されてからはじめてrefinementが有効になります。Mのrefinementがどこで有効になり,どこで無効になるかを次に示します。

  • ファイルaの中
# (無効)
using M
# 有効
class Foo
  # 有効
  def foo
    # 有効
  end
  # 有効
end
# 有効
  • クラスの中
# (無効)
class Foo
  # (無効)
  def foo
    # (無効)
  end
  using M
  # 有効
  def bar
    # 有効
  end
  # 有効
end
# (無効)

Mのrefinementは、クラスFooを後から再度オープンしても自動的に有効になることはありません。

  • evalされる場合
# (無効)
eval <<EOF
  # (無効)
  using M
  # 有効
EOF
# (無効)
  • evalされない場合
# (無効)
if false
  using M
end
# (無効)

同一モジュール内でrefinementを複数定義する場合、同一モジュールのrefineブロック内のrefinementはrefineされたメソッドが呼び出されたタイミングですべて有効になります。

module ToJSON
  refine Integer do
    def to_json
      to_s
    end
  end

  refine Array do
    def to_json
      "[" + map { |i| i.to_json }.join(",") + "]"
    end
  end

  refine Hash do
    def to_json
      "{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
    end
  end
end

using ToJSON  # Intege、Array、Hashはすべてrefinementが有効になる

p [{1=>2}, {3=>4}].to_json # prints "[{\"1\":2},{\"3\":4}]"

メソッド探索順序

Rubyでは、クラスCのインスタンスで次の順序でメソッドを探索します。

  • Cのrefinementが有効な場合、有効になった順序と逆順に探索する
    • Cのrefinementをprependしたモジュール
    • Cのrefinementそのもの
    • Cのrefinementをincludeしたモジュール
  • prependしたCのモジュール
  • クラスCそのもの
  • includeしたCのモジュール

該当のメソッドがどこにもない場合、クラスCのスーパークラスで同じことを繰り返します。

注意

サブクラスのメソッドは、スーパークラスでrefinementされたメソッドよりも優先されます。

たとえば、 #/というメソッドがIntegerのrefinementで定義されているとすると、1 / 2は元のFixnum#/を呼び出します。これは、FixnumがIntegerのサブクラスであり、スーパークラスIntegerにrefinementがあるかどうかを探索するよりも先に探索されるためです。

Integerのrefinementで#fooというメソッドを定義した場合、Fixnumには#fooがないので、1.fooInteger#fooが呼び出されます。

super

superが呼び出されたときのメソッド探索順序は次のようになります。

  • 現在のクラスでincludeしたモジュール(現在のクラスがrefinementの可能性があるため)
  • 現在のクラスがrefinementの場合、前述の順序でメソッド探索を行う
  • 現在のクラスのすぐ上にスーパークラスがある場合、スーパークラスに対して前述の順序でメソッド探索を行う

注意

refinementのメソッド内でsuperを呼ぶと、同じコンテキストで別のrefinementが有効になっていても、そのrefineされたクラス内のメソッドが呼び出されます。

間接的なメソッド呼び出し

Kernel#sendKernel#methodKernel#respond_to?といった「間接的な」メソッドでのアクセスでは、メソッド探索中の呼び出し側のコンテキストでrefinementは考慮されません。

この動作は将来変更される可能性があります。

詳しく知りたい方へ

refinementの実装の現在の仕様やさらに詳しい動作については、https://bugs.ruby-lang.org/projects/ruby-trunk/wiki/RefinementsSpecをご覧ください。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。