Skip to content
Jemma Issroff edited this page Jun 8, 2023 · 2 revisions

Please do not edit this page. File a new ticket instead.

Abstract

This documentation describes the specification of Refinements, which provide local class extensions.

Rationale

Monkey patching is a powerful feature of Ruby.However, it affects globally in a program. Therefore, a monkey patch might break code which doesn't expect the extended behavior, and multiple monkey patches for the same class might cause conflicts.To solve these problems, Refinements provide a way to extend classes locally.

Terms and definitions

  • refinement: A class extension which can be activated only in a certain scope.A refinement is an anonymous module, and is defined under another module, which is used as a namespace for a set of refinements.
  • refine: To extend a class by refinements.
  • refined class: An attribute of a refinement, which represents a class to be extended by the refinement.
  • refine block: A refine block is a block given to Module#refine.
  • defined refinement table: A table which holds refinements defined in a module. Keys are classes to be refined, and values are refinements.
  • main: The object referred by self in toplevel. main is a direct instance of Object, and has singleton methods.
  • implementation-defined: Behavior that possibly differs between implementations, but is defined for every implementation
  • unspecified: Behavior that possibly differs between implementations, and is not necessarily defined for every implementation

Overview

A refinement is defined in a module as follows:

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

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

refine is not a keyword, but a method defined in Module.refine takes a class to be extended as the only argument, and takes a block, in which self is replaced with an anonymous module called a refinement.

A refinement is activated in a certain scope by main.using as follows:

using M
x = C.new
c.foo #=> C#foo in M

In a scope where a refinement is activated, when a method is invoked on an instance of the refined class, the methods defined in the refinement is searched before methods of the refined class.

Defining refinements

A refinement is defined in a module by Module#refine. Multiple refinements can be defined in a single module.

Module#refine(klass, &block)

Visibility: private

Behavior:

Module#refine defines a refinement under the receiver as follows:

  1. If klass is not an instance of Class, raise a TypeError.
  2. Lookup klass in the defined refinement table of the receiver.
  3. If a refinement is found, let R be the refinement.
  4. Otherwise: 4. Create an anonymous module with a special flag which denotes the module is a refinement, and let R be the created anonymous module. 5. Set the refined class of R to klass. 6. Add a pair whose key is klass and whose value is R to the defined refinement table of the receiver
  5. Yield block replacing self with R as module_eval does. Refinements defined in the receiver shall be activated in block as specified in Scope of refinements activated in refine blocks. However, if block is of a Proc, whether refinements are activated is implementation-defined.
  6. Return R.

NOTE: In this specification, modules cannot be refined as described in Step 1 to avoid complexity of method lookup.

Class#refine

Class#refine shall be undefined as if the following Ruby program is evaluated.

class Class
  undef refine
end

NOTE: In this specification, it doesn't make sense to provide Class#refine because class/module scope refinements are not available.

Using refinements

Refinements are activated by main.using.

main.using(mod)

Visibility: private

Behavior:

  1. If using is called in a class/module definition or a method definition, raise a RuntimeError.
  2. If mod is not an instance of Module, or is an instance of Class, raise a TypeError.
  3. Activate refinements in the defined refinement table of mod in a certain scope as specified in Scope of refinements activated by main.using.
  4. Return the receiver.

Scope of refinements

A refinement is activated in a certain scope.The scope of a refinement is lexical in the sense that, when control is transferred outside the scope (e.g., by an invocation of a method defined outside the scope, by load/require, etc...), the refinement is deactivated.In the body of a method defined in a scope where a refinement is activated, the refinement is activated even if the method is invoked outside the scope.

EXAMPLE 1: A refinement is deactivated when control is transferred outside the scope.

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       #=> C#foo in M
call_foo(x) #=> NoMethodError

EXAMPLE 2: A refinement is activated even if a method is invoked outside the scope.

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)  #=> C#foo in M
x.foo               #=> NoMethodError

Scope of refinements activated by main.using

The scope of a refinement activated by main.using is from the point just after main.using is invoked to the end of the file where main.using is invoked.However, when main.using is invoked in a string given as the first argument of Kernel#eval, Kernel#instance_eval, or Module#module_eval, the end of the scope is the end of the string.

main.using activates a refinement at runtime, and therefore a refinement is not activated if main.using is not evaluated.

EXAMPLE 1: using in a file

# not activated here
using M
# activated here
class Foo
  # activated here
  def foo
    # activated here
  end
  # activated here
end
# activated here

EXAMPLE 2: using in eval

# not activated here
eval <<EOF
  # not activated here
  using M
  # activated here
EOF
# not activated here

EXAMPLE 3: using not evaluated

# not activated here
if false
  using M
end
# not activated here

Scope of refinements activated in refine blocks

In a block given to Module#refine, refinements in the defined refinement table of the receiver of Module#refine are activated.The scope of refinements activated in a refine block is only in that block and refinements are deactivated outside the block.

When a method defined in a refine block is invoked, refinements defined in the receiver of Module#refine at the time when the method is invoked are activated.

EXAMPLE 1: Refinements are deactivated outside a refine block.

module StringRecursiveLength
  refine String do
    def recursive_length
      if empty?
        0
      else 
        self[1..-1].recursive_length + 1
      end
    end
  end
  p "abc".recursive_length #=> NoMethodError
end

EXAMPLE 2: Refinements defined at the time when a method is invoked are activated.

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 #=> "[{\"1\":2},{\"3\":4}]"

Method lookup with refinements

Normal method lookup

A method is searched with refinements as follows:

  1. Let N be the name of the method to be invoked.
  2. Let S be the receiver.
  3. If S has a singleton class, let C be the singleton class. Otherwise, let C be the class of S.
  4. If there exist refinements of C (i.e., the refined class of the refinements is C) which are activated in the current context, let RS be the refinements, and take the following steps for each refinement R in RS in the reverse order they are activated in the context:
    1. If R has prepended modules, let MS be the modules, and take the following step for each module M in MS in the reverse order they are prepended into R. 4. If a method with name N found in the method table of M, return the method.
    2. If a method with name N found in the method table of R, return the method.
    3. If R has included modules, let MS be the modules, and take the following step for each module M in MS in the reverse order they are included into R. 3. If a method with name N found in the method table of M, return the method.
  5. If C has prepended modules, let MS be the modules, and take the following step for each module M in MS in the reverse order they are prepended into C. 2. If a method with name N found in the method table of M, return the method.
  6. If a method with name N found in the method table of C, return the method.
  7. If C has included modules, let MS be the modules, and take the following step for each module M in MS in the reverse order they are included into C. 3. If a method with name N found in the method table of M, return the method.
  8. If C has a direct superclass, let C be the superclass, and go to Step 4.
  9. Otherwise, the method is not found.

NOTE: In this specification subclasses have priority over refinements. For example, even if the method / is defined in a refinement of Integer, 1 / 2 invokes the original Fixnum#/ because Fixnum is a subclass of Integer, and is searched before the refinement of Integer. However, if the method foo is defined in a refinement of Integer, 1.foo invokes that method, because foo is not found in Fixnum, and is therefore searched in the refinement.

NOTE: The lookup order for a class C is: refinements of C (and their prepended and included modules) -> prepended modules of C -> C -> included modules of C -> the superclass of C.

super

When super is invoked, a method is search as follows:

  1. Let N be the name of the current method.
  2. Let C be the current class.
  3. If C has included modules, let MS be the modules, and take the following step for each module M in MS in the reverse order they are included into C. 3. If a method with name N found in the method table of M, return the method.
  4. If C is a refinement, search the method N as specified in Normal method lookup from Step 4, where C is the refined class of the refinement.
  5. If C has a direct superclass, search the method N as specified in Normal method lookup from Step 4, where C is the superclass.
  6. Otherwise, the method is not found.

NOTE: In this specification, super in a method of a refinement R invokes the method in the refined class C of R even if there is another refinement for C which has been activated in the same context before R.

Indirect method accesses

Any indirect method access such as Kernel#send, Kernel#method, and Kernel#respond_to? shall not honor refinements in the caller context during method lookup.

NOTE: This behavior will be changed in the future.

Clone this wiki locally