こんにちは、hachi8833です。
自分の理解のためも兼ねて、Ruby 2.1から正式に導入されたRefinementのドキュメントを翻訳しました。適宜強調などを行っていますのでご了承ください。訳語はほぼまったく定着していないので、英語のrefinementで表記します。
refinementの理解は#using
メソッドの動作とスコープの理解にかかっていると感じました。#using
を書いた位置から下でrefinementが効くというあたりは、(機能は違いますが)private
キーワードと少し似ているように思います。
当時は知りませんでしたが、refinementの導入はかなり大変だったようです。以下も合わせてどうぞ。
- るびま: Refinementsとは何だったのか
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
したモジュール
- Cのrefinementを
prepend
したCのモジュール- クラスCそのもの
include
したCのモジュール
該当のメソッドがどこにもない場合、クラスCのスーパークラスで同じことを繰り返します。
注意
サブクラスのメソッドは、スーパークラスでrefinementされたメソッドよりも優先されます。
たとえば、 #/
というメソッドがIntegerのrefinementで定義されているとすると、1 / 2
は元のFixnum#/
を呼び出します。これは、FixnumがIntegerのサブクラスであり、スーパークラスIntegerにrefinementがあるかどうかを探索するよりも先に探索されるためです。
Integerのrefinementで#foo
というメソッドを定義した場合、Fixnumには#foo
がないので、1.foo
でInteger#foo
が呼び出されます。
super
super
が呼び出されたときのメソッド探索順序は次のようになります。
- 現在のクラスで
include
したモジュール(現在のクラスがrefinementの可能性があるため) - 現在のクラスがrefinementの場合、前述の順序でメソッド探索を行う
- 現在のクラスのすぐ上にスーパークラスがある場合、スーパークラスに対して前述の順序でメソッド探索を行う
注意
refinementのメソッド内でsuper
を呼ぶと、同じコンテキストで別のrefinementが有効になっていても、そのrefine
されたクラス内のメソッドが呼び出されます。
間接的なメソッド呼び出し
Kernel#send
、Kernel#method
、Kernel#respond_to?
といった「間接的な」メソッドでのアクセスでは、メソッド探索中の呼び出し側のコンテキストでrefinementは考慮されません。
この動作は将来変更される可能性があります。
詳しく知りたい方へ
refinementの実装の現在の仕様やさらに詳しい動作については、https://bugs.ruby-lang.org/projects/ruby-trunk/wiki/RefinementsSpecをご覧ください。