diff options
Diffstat (limited to 'doc/syntax/refinements.rdoc')
| -rw-r--r-- | doc/syntax/refinements.rdoc | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/doc/syntax/refinements.rdoc b/doc/syntax/refinements.rdoc new file mode 100644 index 0000000000..80595eb445 --- /dev/null +++ b/doc/syntax/refinements.rdoc @@ -0,0 +1,299 @@ += Refinements + +Due to Ruby's open classes you can redefine or add functionality to existing +classes. This is called a "monkey patch". Unfortunately the scope of such +changes is global. All users of the monkey-patched class see the same +changes. This can cause unintended side-effects or breakage of programs. + +Refinements are designed to reduce the impact of monkey patching on other +users of the monkey-patched class. Refinements provide a way to extend a +class locally. Refinements can modify both classes and modules. + +Here is a basic refinement: + + class C + def foo + puts "C#foo" + end + end + + module M + refine C do + def foo + puts "C#foo in M" + end + end + end + +First, a class +C+ is defined. Next a refinement for +C+ is created using +Module#refine. + +Module#refine creates an anonymous module that contains the changes or +refinements to the class (+C+ in the example). +self+ in the refine block is +this anonymous module similar to Module#module_eval. + +Activate the refinement with #using: + + using M + + c = C.new + + c.foo # prints "C#foo in M" + +== Scope + +You may activate refinements at top-level, and inside classes and modules. +You may not activate refinements in method scope. Refinements are activated +until the end of the current class or module definition, or until the end of +the current file if used at the top-level. + +You may activate refinements in a string passed to Kernel#eval. Refinements +are active until the end of the eval string. + +Refinements are lexical in scope. Refinements are only active within a scope +after the call to +using+. Any code before the +using+ statement will not have the +refinement activated. + +When control is transferred outside the scope, the refinement is deactivated. +This means that if you require or load a file or call a method that is defined +outside the current scope the refinement will be deactivated: + + class C + end + + module M + refine C do + def foo + puts "C#foo in M" + end + end + end + + def call_foo(x) + x.foo + end + + using M + + x = C.new + x.foo # prints "C#foo in M" + call_foo(x) #=> raises NoMethodError + +If a method is defined in a scope where a refinement is active, the refinement +will be active when the method is called. This example spans multiple files: + +c.rb: + + class C + end + +m.rb: + + require "c" + + module M + refine C do + def foo + puts "C#foo in M" + end + end + end + +m_user.rb: + + require "m" + + using M + + 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) # prints "C#foo in M" + x.foo #=> raises NoMethodError + +Since the refinement +M+ is active in <code>m_user.rb</code> where +<code>MUser#call_foo</code> is defined it is also active when +<code>main.rb</code> calls +call_foo+. + +Since #using is a method, refinements are only active when it is called. Here +are examples of where a refinement +M+ is and is not active. + +In a file: + + # not activated here + using M + # activated here + class Foo + # activated here + def foo + # activated here + end + # activated here + end + # activated here + +In a class: + + # not activated here + class Foo + # not activated here + def foo + # not activated here + end + using M + # activated here + def bar + # activated here + end + # activated here + end + # not activated here + +Note that the refinements in +M+ are *not* activated automatically if the class ++Foo+ is reopened later. + +In eval: + + # not activated here + eval <<EOF + # not activated here + using M + # activated here + EOF + # not activated here + +When not evaluated: + + # not activated here + if false + using M + end + # not activated here + +When defining multiple refinements in the same module inside multiple +refine+ blocks, +all refinements from the same module are active when a refined method +(any of the +to_json+ methods from the example below) is called: + + 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 + + p [{1=>2}, {3=>4}].to_json # prints "[{\"1\":2},{\"3\":4}]" + + +== Method Lookup + +Method lookup in Ruby is based on the ancestor chain. You can see the +ancestor chain for any object in Ruby by doing: + + object.singleton_class.ancestors + # or, if the object does not support a singleton class: + object.class.ancestors + +The ancestor chain is constructed as follows: + +* Subclasses are before superclasses in the ancestor chain +* Prepended modules are before the class they prepend in the ancestor + chain, in reverse order in which they were prepended. +* Included modules are after the class they are included in in the + ancestor chain, in reverse order in which they were included. + +When looking up a method for an object, Ruby goes through each ancestor: + +* If the class/module has been refined, Ruby will consider the refinements + activated at the point the method was called, in reverse order of + activation. +* Otherwise, Ruby will check the methods of the class/module itself. + +If no method was found at either point this repeats with the next +ancestor. + +Note that methods in a earlier ancestor have priority over refinements in a +later ancestor. For example, if the method <code>/</code> is defined in a +refinement for Numeric <code>1 / 2</code> invokes the original Integer#/ +because Integer is a comes before Numeric in the ancestor chain. However, +if a method +foo+ is defined on Numeric in a refinement, <code>1.foo</code> +invokes that method since +foo+ does not exist on Integer. + +== +super+ + +When +super+ is invoked, method lookup starts: + +* If the method is in a refinement, at the refined class or module +* Otherwise, at the next ancestor + +Method lookup then proceeds as described in the Method Lookup section +above. + +Refinements activated at the call site of a refinement method do not +affect +super+ inside that method. Only refinements activated at the +point +super+ was called affect method lookup for that +super+ call. +You cannot use refinements to insert into the middle of a method +lookup chain, only to insert at the start of a method lookup chain, +unless you control the +super+ call sites. + +Note that if you refine a module, the refinement method can call +super+ +to call the method in the module, but the method in the module cannot +call +super+ to continue the method lookup process to further ancestors. + +== Methods Introspection + +When using introspection methods such as Kernel#method or Kernel#methods refinements are not honored. + +This behavior may be changed in the future. + +== Refinement inheritance by Module#include + +When a module X is included into a module Y, Y inherits refinements from X. + +For example, C inherits refinements from A and B in the following code: + + module A + refine X do ... end + refine Y do ... end + end + module B + refine Z do ... end + end + module C + include A + include B + end + + using C + # Refinements in A and B are activated here. + +Refinements in descendants have higher precedence than those of ancestors. + +== Further Reading + +See https://github.com/ruby/ruby/wiki/Refinements-Spec for the +current specification for implementing refinements. The specification also +contains more details. |
