Refinements and Monkey Patching
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them.
Ruby classes are open: any program can reopen String or Integer and add, replace or remove methods, and the change is
visible to every line of Ruby (and every library) running in the process. That is called monkey patching, and it is
both the technique behind much of Ruby’s expressiveness and the source of some of its hardest-to-debug failures.
Refinements (Module#refine and using) are the language’s answer: a way to extend a class that is visible only in
the files and class bodies that ask for it. This page covers the whole subject: what a monkey patch is and how to find
and tame one, Module#prepend as the patch that can call the original, refinements with their lexical scope rules, the
method lookup order and super, the list of core methods that honor (and do not honor) refinements, import_methods,
introspection, testing, the experimental Ruby::Box that isolates patches at process level, and a guide for choosing
between all of these.
It builds on Classes and Objects,
Modules and Mixins (include, prepend and extend) and
Method Lookup and super, which explains the ancestor chain
that refinements are inserted in front of. The mechanics that make patching possible are in
Singleton Classes and Class-Level Behavior,
Dynamic Dispatch and Method Definition and
Hooks, eval and DSLs; how to look at what a patch changed is in
Reflection, Introspection and Tracing.
Visibility is covered in
Visibility and Encapsulation, and the conversion
methods that C code calls (to_str, to_ary) in
Conversions and Coercion. How files are loaded, and
therefore where a refinement is active, is in
Code Loading: require and autoload. Testing
patched code is in Testing with Minitest, and the usual mistakes
are collected in Common Mistakes and Gotchas.
For the same need in other languages, see
C# extension members,
Kotlin extension functions,
Swift extensions,
Scala extension methods,
Groovy extension modules and
Python decorators and metaclasses.
Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with
RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings,
NewCops: enable, Style/Documentation off). The output shown is the real output. The examples that need the gem are
run with ruby -Ilib, so require "shelf" finds it (see the shelf gem source); the shelf gem itself ships one refinement,
Shelf::Refinements::TitleCase, which the examples reuse. One example (the collision in
Collisions between patches) deliberately triggers a Ruby warning, and two carry a rubocop:disable comment for a
deliberate illustration; each is called out where it appears.
Open classes and monkey patching
Ruby never closes a class. A class or module statement for a name that already exists reopens it, and every def
inside adds a method or replaces an existing one. The classes shipped with Ruby are no exception: String, Integer,
Array, Hash, Kernel and even BasicObject can be reopened. A change made this way to a class that you do not own
is a monkey patch.
# frozen_string_literal: true
# A monkey patch: reopen a core class and add a method. Every caller sees it.
class String
def title_case = split.map(&:capitalize).join(" ")
end
puts "the left hand of darkness".title_case
puts "a wizard of earthsea".respond_to?(:title_case)
puts String.method_defined?(:title_case)
puts String.instance_method(:title_case).owner
puts %w[ico stone].map(&:title_case).inspect
The Left Hand Of Darkness
true
true
String
["Ico", "Stone"]
Source: Modules and Classes (syntax)
The patch is a plain method definition, so everything in
Methods and Parameters applies, and there is no special mark
on the method. From the moment the class String body runs, title_case is an ordinary public instance method of
String, owned by String itself (Method#owner prints String). Every piece of code in the process, including gems
loaded later and code that was loaded earlier, can now call it, and respond_to? says yes.
Monkey patching is a legitimate tool. It is how a library adds to_json to the core classes after require "json", how
a polyfill backports a method from a newer Ruby, and how a test double replaces a method for the duration of a test.
The Ruby documentation says plainly what the cost is: the scope of the change is global, so all users of the patched
class see the same changes, which can cause unintended side effects or breakage of programs. The sections below name
those costs one by one.
The costs of a global change
| Hazard | What happens |
|---|---|
Global reach |
The patch is visible to the application, to every gem, and to the standard library. Code written years ago, that never heard of the patch, now runs against the new behavior. |
Collisions |
Two libraries (or a library and a newer Ruby) define the same method. The definition that loads last wins, silently unless |
Load-order dependence |
The behavior of a method depends on which |
Reach into C code |
Replacing a method that the interpreter’s own C code calls ( |
Invisible provenance |
Nothing in |
Hard to remove |
Removing a patch means finding every caller that came to rely on it. |
Hard to test |
A patch applied by one test file leaks into every other test in the process, so order-dependent failures appear. |
Collisions between patches
When two libraries define the same method on the same class, the later definition replaces the earlier one. With the
-w flag (or $VERBOSE = true) Ruby reports the replacement on standard error with the file and line of both
definitions; without the flag nothing is printed. The warning is deliberately triggered here to show what it looks like.
# frozen_string_literal: true
# Two libraries patch the same method. The last definition loaded wins.
module Libraries
def self.load_a = String.class_eval { def slug = downcase.tr(" ", "-") }
def self.load_b = String.class_eval { def slug = downcase.gsub(/\W+/, "_") }
end
Libraries.load_a
Libraries.load_b
puts "The Left Hand of Darkness".slug
# Find out where a method was defined.
location = "x".method(:slug).source_location
puts "#{File.basename(location[0])}:#{location[1]}"
p String.instance_method(:upcase).source_location
p String.method_defined?(:slug, false)
ex02.rb:7: warning: method redefined; discarding old slug
ex02.rb:5: warning: previous definition of slug was here
the_left_hand_of_darkness
ex02.rb:7
nil
true
Source: Method#source_location
Three tools find out who owns what. Method#source_location returns the file and line of a Ruby-defined method (and
nil for one implemented in C, as String#upcase is). Module#method_defined? with the second argument false
answers whether the class itself, not an ancestor, defines the method. And Method#owner names the class or module whose
method table holds the method. RuboCop’s Lint/DuplicateMethods finds the same mistake statically inside one file.
A patched method that must keep working with every caller should also keep the signature of the original; the safest
way to guarantee that is to forward everything with *, ** and & (see
Calling Methods and Argument Forwarding).
What a monkey patch reaches
The reach of a patch includes the interpreter’s own C code. The example adds to_str and to_ary to a class that had
neither. Before the patch, Array#join, string concatenation and multiple assignment do not treat the object as a
string or an array; after it, they do, because those built-in methods ask the object for the implicit conversions (see
Conversions and Coercion).
# frozen_string_literal: true
# The same additions as a plain monkey patch: every caller, Ruby or C, sees them.
module Demo
class Receipt
def to_s = "plain"
end
end
receipt = Demo::Receipt.new
puts [receipt].join(",")
first, second = receipt
p [first.to_s, second]
Demo::Receipt.class_eval do
def to_str = "patched-str"
def to_ary = %w[patched ary]
end
puts [receipt].join(",")
puts(+"text " << receipt)
first, second = receipt
p [first, second]
plain
["plain", nil]
patched-str
text patched-str
["patched", "ary"]
Source: Module#class_eval
Keep this contrast in mind for the refinement sections: a refinement of the same two methods is not seen by
Array#join or by multiple assignment (see Which core methods honor refinements).
alias_method chains and prepend
Before refinements, before even Module#prepend (Ruby 2.0), a patch that wanted to call the original method used an
alias chain: alias the original to a new name, define the new method, and call the alias. The technique works once and
breaks when the patch is loaded twice (a load in a console, a reloading development server, two gems applying the
same patch), because the second alias captures the already patched method, which calls the alias, which is now the
patched method: unbounded recursion.
Module#prepend inserts a module in front of the class in the ancestor chain, so the module’s method runs first and
super reaches the original. Prepending a module that is already in the ancestors does nothing, so the patch is
idempotent.
# frozen_string_literal: true
module Demo
# The older technique, alias_method chaining, breaks when applied twice.
class Receipt
def to_s = "receipt"
end
def self.apply_alias_patch
Receipt.class_eval do
alias_method :original_to_s, :to_s
def to_s = "#{original_to_s}!"
end
end
# prepend is safe to apply twice: the module is already in the ancestors.
module Stamp
def to_s = "#{super}!"
end
class Ticket
def to_s = "ticket"
end
end
Demo.apply_alias_patch
puts Demo::Receipt.new
Demo.apply_alias_patch
begin
Demo::Receipt.new.to_s
rescue SystemStackError => e
puts "#{e.class}: #{e.message}"
end
2.times { Demo::Ticket.prepend(Demo::Stamp) }
puts Demo::Ticket.new
p Demo::Ticket.ancestors.first(3)
receipt!
SystemStackError: stack level too deep
ticket!
[Demo::Stamp, Demo::Ticket, Object]
Source: Module#prepend
The same technique patches a class from the shelf gem. Shelf::Book is a Data class (see
Struct, Data and Value Objects) whose block defines
to_s. A prepended module wraps it without editing the gem, and Method#super_method and ancestors show the
arrangement.
# frozen_string_literal: true
require "shelf"
# prepend puts the patch in front of the class, so super reaches the original.
module Shouting
def to_s = "#{super}!"
end
isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Ubik", author: Shelf::Author.new(name: "Philip K. Dick"))
puts book
Shelf::Book.prepend(Shouting)
puts book
p Shelf::Book.ancestors.first(3)
p Shelf::Book.instance_method(:to_s).owner
p Shelf::Book.instance_method(:to_s).super_method.owner
Ubik by Philip K. Dick
Ubik by Philip K. Dick!
[Shouting, Shelf::Book, Data]
Shouting
Shelf::Book
Source: Method#super_method
A prepended patch is still global, but it is explicit (a named module with its own file), visible in
ancestors, composable, removable in principle (not at run time: Ruby has no unprepend) and able to call the original
with super. It is the recommended form of a global patch.
A defensive patching checklist
-
Prefer not to patch: use a function,
extendon one object, or a wrapper (see Alternatives to patching a core class). -
If a global change is unavoidable, make it a named module and
prependit, so the original stays reachable withsuper. -
Add only methods that do not exist, and guard them, so a newer Ruby or another library wins (the polyfill below).
-
Keep every patch in one file or directory per class (for example
core_ext/string.rb), documented and tested. -
Choose names unlikely to collide (a library prefix), forward arguments completely, and keep the return type of the original.
-
Run the test suite with
-wso redefinition warnings are visible. -
Never replace methods that the interpreter or the standard library call (
to_s,inspect,hash,eql?,each,coerce) in a way that changes their contract.
A polyfill adds a method only when it is missing. This is the one global patch that is nearly always safe, because it does nothing on a Ruby that already has the method.
# frozen_string_literal: true
# A guarded monkey patch: define the method only when it is missing,
# so a newer Ruby or another library wins.
module Polyfill
def self.install(klass, name, &)
return if klass.method_defined?(name) || klass.private_method_defined?(name)
klass.define_method(name, &)
end
end
p Polyfill.install(String, :each_char_pair) { each_char.each_slice(2).map(&:join) }
p Polyfill.install(String, :each_char_pair) { :never }
p Polyfill.install(Array, :sum) { :never }
p "isbn".each_char_pair
:each_char_pair
nil
nil
["is", "bn"]
Source: Module#method_defined?
Alternatives to patching a core class
Most of the time the need behind a monkey patch ("I want title_case on strings") can be met without changing String.
# frozen_string_literal: true
require "delegate"
# Alternatives that do not touch String for everybody.
module Titles
def self.call(text) = text.split.map(&:capitalize).join(" ")
def title_case = Titles.call(self)
# A wrapper that forwards everything else to the wrapped string.
class Heading < SimpleDelegator
def title_case = Titles.call(__getobj__)
end
end
# 1. A function.
puts Titles.call("the left hand of darkness")
# 2. extend on one object: only that string gains the method.
heading = (+"a wizard of earthsea").extend(Titles)
puts heading.title_case
puts "other".respond_to?(:title_case)
# 3. The wrapper.
puts Titles::Heading.new("the dispossessed").title_case
puts Titles::Heading.new("the dispossessed").length
The Left Hand Of Darkness
A Wizard Of Earthsea
false
The Dispossessed
16
Source: Object#extend
| Technique | Scope of the change | Costs |
|---|---|---|
A module function ( |
None: nothing is changed. |
The call site reads less fluently. |
|
That one object (it gains a singleton class). |
The object gains a singleton class, and every other object of the class is unaffected. |
A wrapper such as |
Only code that uses the wrapper. |
The wrapper is a different object ( |
A subclass |
Only instances created from it. |
Cannot change objects created elsewhere, such as literals. |
|
Global, for every instance of the class. |
A monkey patch that is at least explicit and can call |
A refinement |
The files and class bodies that call |
Lexical rules and a few unrefined core methods (see below). |
|
A whole box (experimental in 4.0). |
Experimental; breaks code that relies on global definitions. |
Refinements
A refinement extends a class (or module) locally. It is defined in a module with Module#refine and switched on with
using. The definition lives in a block whose self is an anonymous module, a Refinement, in which ordinary def
statements define the new or replacing methods. The shelf gem has one, written exactly like this:
# frozen_string_literal: true
module Shelf
module Refinements
# Refines String instead of monkey patching it.
module TitleCase
refine String do
def title_case = split.map(&:capitalize).join(" ")
end
end
end
end
Source: Module#refine
The documentation describes the idea in three sentences: refinements reduce the impact of monkey patching on other
users of the patched class, provide a way to extend a class locally, and can modify both classes and modules. Using the
refinement is one line, using Shelf::Refinements::TitleCase, and from that line on the new method exists for code
written below it.
# frozen_string_literal: true
require "shelf"
begin
"the left hand of darkness".title_case
rescue NoMethodError => e
puts e.message
end
using Shelf::Refinements::TitleCase
puts "the left hand of darkness".title_case
puts "a wizard of earthsea".respond_to?(:title_case)
puts "ursula".send(:title_case)
p %w[ico stone].map(&:title_case)
p String.method_defined?(:title_case)
p "x".methods.include?(:title_case)
undefined method 'title_case' for an instance of String
The Left Hand Of Darkness
true
Ursula
["Ico", "Stone"]
false
false
Source: Refinements (syntax)
Notice what the output shows. The call written before using fails, with the usual NoMethodError message (Ruby
3.3 and later print for an instance of String, not the receiver’s inspect). After using, the method works, including
through send and Symbol#to_proc. And the class itself is untouched: String.method_defined? is false and
"x".methods does not list title_case. Reflection that lists methods does not see refinements.
The anatomy of a refinement
| Piece | Rule |
|---|---|
|
Private method of |
Where |
In the body of a module. |
The block |
Evaluated like |
Several |
Independent refinements (one per target). All of the module’s refinements become active together with one |
|
Activates every refinement of |
|
The class of the object |
|
Deprecated in Ruby 3.1 and removed in 3.2; they raise |
|
Accepted, but it extends the |
A refinement can replace an existing method as well as add one. It can refine operators (the refined Integer#+ is used
by 1 + 1 in the using scope), Kernel methods and the singleton methods of a class (refine String.singleton_class),
as the next example shows with a module refinement and a class-method refinement bundled by including one refinement
module into another.
# frozen_string_literal: true
# Refine a module, a singleton class, and bundle refinements by including
# one refinement module into another.
module Extras
module Lists
refine Enumerable do
def second = drop(1).first
end
end
module Parsing
refine String.singleton_class do
def from_pairs(pairs) = pairs.map { |k, v| "#{k}=#{v}" }.join("&")
end
end
module All
include Lists
include Parsing
end
end
using Extras::All
p [10, 20, 30].second
p (1..5).second
p String.from_pairs(page: 2, per: 10)
p Extras::All.refinements
20
2
"page=2&per=10"
[]
Module#include between refinement-holding modules is supported and documented: when a module X is included into a
module Y, Y inherits the refinements of X, and refinements in descendants have higher precedence than those of ancestors.
That lets a library publish one module (Extras::All) that activates a set of refinements with a single using. Note that
Extras::All.refinements is empty: Module#refinements lists only the refinements the module defines itself.
Scope: where a refinement is active
Refinements are lexical. The documentation states the rules:
-
A refinement can be activated at the top level of a file and inside a class or module body, never inside a method body.
-
It stays active until the end of the current class or module body, or until the end of the file for a top-level
using. -
Code before the
usingcall does not see it. This includes methods defined earlier in the same file. -
A method defined where a refinement is active keeps it active when it runs, wherever it is called from.
-
A method defined outside the scope does not get the refinement, even when it is called from inside the scope.
-
require,loadand calls into other files leave the scope, so a refinement is not visible in them. -
Reopening a class later does not reactivate the refinement in the new body.
The first example shows the file-level rules. plain_title is defined before using; refined_title after. Symbol#to_proc
applied at a call site below using sees the refinement, as the last line shows.
# frozen_string_literal: true
require "shelf"
# A refinement is lexical: it is active from the using call to the end of the
# file (at top level) or of the class or module body (inside one).
def plain_title(text) = text.title_case
using Shelf::Refinements::TitleCase
def refined_title(text) = text.title_case
def attempt
yield
rescue NoMethodError => e
e.message
end
puts "ico".title_case
puts refined_title("stone of tears")
puts(attempt { plain_title("ico") })
puts(attempt { [["ico"]].flatten.map(&:title_case) })
Ico
Stone Of Tears
undefined method 'title_case' for an instance of String
Ico
Source: Refinements: Scope
The second example walks through the remaining cases: a class (here a module) body, reopening it, the ban on using in a
method, eval with a string, and a using in a statement that never runs. It deliberately reopens Heading in the same
file, which RuboCop’s Style/OneClassPerFile reports; the cop is disabled on that one line.
# frozen_string_literal: true
require "shelf"
def attempt
yield
rescue NoMethodError => e
e.message
end
module Heading
def self.before(text) = attempt { text.title_case }
using Shelf::Refinements::TitleCase
def self.after(text) = attempt { text.title_case }
end
puts Heading.before("ursula")
puts Heading.after("ursula")
# Reopening the module does not reactivate the refinement.
module Heading # rubocop:disable Style/OneClassPerFile
def self.again(text) = attempt { text.title_case }
end
puts Heading.again("ursula")
# using is not allowed in a method body.
def activate = using(Shelf::Refinements::TitleCase)
begin
activate
rescue RuntimeError => e
puts "#{e.class}: #{e.message}"
end
# using inside eval lasts until the end of the eval string.
puts eval(<<~RUBY, nil, __FILE__, __LINE__ + 1)
using Shelf::Refinements::TitleCase
"in eval: " + "le guin".title_case
RUBY
puts "after eval: #{attempt { 'le guin'.title_case }}"
# using in a branch that never runs activates nothing.
using Shelf::Refinements::TitleCase if ENV.key?("NEVER_SET_SHELF")
puts "after if: #{attempt { 'le guin'.title_case }}"
undefined method 'title_case' for an instance of String
Ursula
undefined method 'title_case' for an instance of String
RuntimeError: main.using is permitted only at toplevel
in eval: Le Guin
after eval: undefined method 'title_case' for an instance of String
after if: undefined method 'title_case' for an instance of String
Source: Refinements: Scope
Notes on the outcomes:
-
Heading.beforeis defined above theusingin the module body: it fails.Heading.afteris defined below: it works. -
Reopening
Headingstarts a new body in which the refinement is not active:againfails. -
usinginside a method is rejected at run time withRuntimeError(main.using is permitted only at toplevelwhen the method is defined at top level).usingis a method call, so ausingthat never executes (anifthat is not taken) activates nothing. -
usingin anevalstring works for the code of the string and ends with it. The documentation says "Refinements are active until the end of the eval string". Passing an explicitBindingas the second argument is different on 4.0.7: the refinement then remains active for the code of that binding’s scope after theevalreturns. The behavior is not part of the documented rules; do not rely on it. -
String-evaluated code (
eval,instance_evalwith a string) runs with the refinements that are active at the call site (checked on 4.0.7).
Several files
The rules matter most across files. A using in a file applies to every method that file defines, however far away the
caller is, and does not leak into the file that requires it. This is why a library can use a refinement internally without
imposing it on its users.
Three files: the refinement, a view that activates it and defines a method, and the main script.
# frozen_string_literal: true
module Titles
refine String do
def headline = split.map(&:capitalize).join(" ")
end
end
# frozen_string_literal: true
require_relative "titles"
using Titles
# The refinement is active in this file, so it is active whenever
# HeadingView#render runs, no matter who calls it.
class HeadingView
def render(text) = "<h1>#{text.headline}</h1>"
end
# frozen_string_literal: true
require_relative "heading_view"
puts HeadingView.new.render("the lathe of heaven")
begin
"the lathe of heaven".headline
rescue NoMethodError => e
puts e.message
end
<h1>The Lathe Of Heaven</h1>
undefined method 'headline' for an instance of String
Source: Refinements: Scope
HeadingView#render was defined in a file where using Titles is active, so it can call headline when main.rb calls it,
though main.rb never activated the refinement and cannot call headline itself.
| Code position | Refinement active? |
|---|---|
In a file, before |
No |
In a file, after |
Yes, until the end of the file |
In a class body, before |
No |
In a class body, after |
Yes, until the end of that class body |
Inside a method body |
|
In the |
Yes, until the end of the string |
In a file loaded with |
No |
In a class reopened after the |
No |
Method lookup and super with refinements
Refinements are consulted before the ordinary lookup for the class they refine. For a call on an instance of class C in a scope where refinements of C are active, Ruby checks, in order:
-
the refinements of C, in the reverse order in which they were activated (the latest
usingfirst); -
the modules prepended to C;
-
C itself;
-
the modules included in C.
If nothing is found, the search repeats with the superclass of C. The ordinary rules (singleton classes, ancestors,
prepend) are the subject of Method Lookup and super.
The example builds the full chain: a class with a prepended module, a subclass relation, two refinements of the same class activated one after the other, and a refinement of the superclass.
# frozen_string_literal: true
# Lookup order for a call: refinements (latest first), prepended modules,
# the class, included modules, then the superclass.
module Lookup
module Tagged
def label = "prepended(#{super})"
end
class Edition
def label = "class"
end
class Hardcover < Edition
prepend Tagged
def label = "hardcover(#{super})"
end
module First
refine Hardcover do
def label = "first(#{super})"
end
end
module Second
refine Hardcover do
def label = "second(#{super})"
end
end
# A refinement of the superclass.
module OnEdition
refine Edition do
def label = "refined edition"
def kind = "refined kind"
end
end
end
copy = Lookup::Hardcover.new
puts copy.label
using Lookup::First
puts copy.label
using Lookup::Second
puts copy.label
using Lookup::OnEdition
puts copy.label
puts copy.kind
prepended(hardcover(class))
first(prepended(hardcover(class)))
second(prepended(hardcover(class)))
second(prepended(hardcover(class)))
refined kind
Source: Refinements: Method Lookup
Reading the output:
-
Without refinements: the prepended
Taggedruns first, thenHardcover, whosesuperreachesEdition. -
After
using Lookup::First, the refinement runs first and itssupercalls the refined class’s method, which still starts atTagged. -
After
using Lookup::Second,Secondis consulted beforeFirst. Itssuperalso calls the refined class’s method (notFirst), soFirstis skipped. The documentation states this:superin a method of a refinement invokes the method in the refined class even if another refinement is active in the same context. -
After
using Lookup::OnEdition(a refinement of the superclass),copy.labelis unchanged: the subclass defineslabel, so the search ends before the superclass’s refinements are reached. ThesuperinsideHardcover#labelis in a file region whereOnEditionis not active, so it reaches the plainEdition#label. -
copy.kindis a method that exists only in the refinement of the superclass, so it is found.
The documentation gives the same rule with numbers. A refinement of Numeric that defines / is never used for an
Integer, because Integer#/ exists and the lookup stops at Integer; but a refinement that defines a new method
on Numeric is found for an Integer receiver.
# frozen_string_literal: true
# Numeric is the superclass of Integer. Integer#/ exists, so a refined
# Numeric#/ is never reached for an Integer, but a new Numeric method is.
module NumericExtras
refine Numeric do
def /(_other) = "refined"
def half = self / 2
end
end
using NumericExtras
puts 1 / 2
puts 5.half
puts 2.5.half
0
2
1.25
Source: Refinements: Method Lookup
Here 1 / 2 is 0 (the unrefined Integer#/), 5.half is 2 (it calls self / 2, and the / it reaches inside the
refinement block is still Integer#/), and 2.5.half is 1.25: Float#/ exists too.
The rules for super in the documentation, in full: from a method of a refinement, super looks first at the modules
included in the refinement; then, since the current class is a refinement, the lookup proceeds as above for the refined
class (the "Method Lookup" list); then the superclass. A super in a method of a module that is merely included in a
refinement does not follow the refinement rule.
Which core methods honor refinements
A refinement is active for Ruby code that is lexically inside the using scope. Calls made from Ruby code outside that scope,
and calls made by the interpreter’s C code (Array#join, puts, the destructuring in a, b = obj, Hash lookups),
do not see it. Over the years Ruby taught a short list of core methods to honor them:
| Since | What changed |
|---|---|
2.0 / 2.1 |
Refinements honored by ordinary method calls with an explicit or implicit receiver, in the using scope (experimental in 2.0, stable in 2.1). |
2.4 |
Honored by |
2.5 |
Honored in string interpolation (the |
2.6 |
Honored at block passing ( |
2.7 |
Honored by |
3.1 |
|
3.2 |
|
3.3 |
|
3.4 |
|
# frozen_string_literal: true
# What does a refinement reach? Ruby code that is lexically inside the
# using scope, and a short list of core methods that were taught about them.
module Demo
class Receipt
def to_s = "plain"
def inspect = "plain-inspect"
def to_str = "plain-str"
def to_ary = %i[plain ary]
end
module Fancy
refine Receipt do
def to_s = "fancy"
def inspect = "fancy-inspect"
def to_str = "fancy-str"
def to_ary = %i[fancy ary]
def total = 42
end
end
end
using Demo::Fancy
receipt = Demo::Receipt.new
name = :total
puts "-- honored"
puts "receipt: #{receipt}"
puts receipt.total
puts receipt.send(name)
puts receipt.public_send(name)
puts receipt.respond_to?(name)
puts [receipt].map(&:total).inspect
puts receipt.method(name).call
puts Demo::Receipt.instance_method(name).bind_call(receipt)
puts "-- not honored"
puts [receipt].join(",")
puts(+"text " << receipt)
p receipt
first, second = receipt
p [first, second]
p receipt.methods.include?(name)
p Demo::Receipt.method_defined?(name)
-- honored
receipt: fancy
42
42
42
true
[42]
42
42
-- not honored
plain-str
text plain-str
plain-inspect
[:plain, :ary]
false
false
The "honored" half: a normal call, string interpolation, send, public_send, respond_to?, map(&:total), method
and instance_method all see the refinement. The "not honored" half: Array#join and string concatenation call
to_str, p calls inspect, destructuring calls to_ary: each of these is C code outside the lexical scope, so the
original methods run (the output prints the plain values, not the refined ones). The listing methods
(methods, method_defined?) do not include refined methods either. The documentation says that introspection methods
such as Kernel#method or Kernel#methods do not honor refinements and that this "may be changed in the future"; in
practice method and instance_method have honored them since 2.7 and the listing methods still do not. Test the behavior you
depend on.
The differences in daily use:
| Feature | Monkey patch | Refinement |
|---|---|---|
|
Yes |
Yes, in the using scope |
|
Yes |
No |
|
Yes |
No |
|
Yes |
No |
|
Yes |
Yes (2.5) |
|
Yes |
No |
|
Yes |
Yes (2.4 / 2.6) |
|
Yes |
Yes (2.6) |
|
Yes |
No |
Visible to other files and gems |
Yes |
No |
Can be undone |
No |
By leaving the scope |
import_methods
A refinement cannot include a module (an error since the deprecation in 3.1). The replacement, Refinement#import_methods
(Ruby 3.1), copies the methods of one or more modules into the refinement. Because the copy is compiled as part of the
refinement, the refinement is active inside the imported methods: they can call the other refined methods of the same
refinement. Methods that are only included normally never see it.
The documentation adds the restriction that follows from copying: only methods written in Ruby can be imported. C-defined
methods (most of Enumerable, for instance) raise ArgumentError naming the first one it meets.
# frozen_string_literal: true
# A module cannot be included into a refinement; import_methods copies its
# Ruby-defined methods so that they are compiled with the refinement active.
module Imports
module Padding
def pad(width) = ljust(width, ".")
def padded_title(width) = title_case.then { |s| s.ljust(width, ".") }
end
module Display
refine String do
def title_case = split.map(&:capitalize).join(" ")
import_methods Padding
end
end
end
using Imports::Display
puts "dune".pad(8)
puts "the dispossessed".padded_title(20)
# Mixed in normally, the module's methods were compiled without the refinement.
plain = (+"dune").extend(Imports::Padding)
begin
plain.padded_title(8)
rescue NameError => e
puts "#{e.class}: #{e.name}"
end
# Only methods written in Ruby can be imported.
begin
Module.new do
refine(String) { import_methods Enumerable }
end
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
begin
Module.new do
refine(String) { include Comparable } # rubocop:disable Lint/RefinementImportMethods
end
rescue TypeError => e
puts "#{e.class}: #{e.message}"
end
dune....
The Dispossessed....
NameError: title_case
ArgumentError: Can't import method which is not defined with Ruby code: Enumerable#each_cons
TypeError: Refinement#include has been removed
Source: Refinement#import_methods
Padding#padded_title calls title_case with an implicit receiver. When mixed into plain through extend, it was
compiled outside the refinement and fails with a NameError (a bare name that is neither a local variable nor a method
reads as a variable-or-method reference, hence NameError, not NoMethodError). Imported into Display, it is compiled with the refinement active and works.
The include Comparable in the last block is the deliberate illustration of the removed form; RuboCop’s
Lint/RefinementImportMethods flags it and the line carries a rubocop:disable comment.
Introspection
The introspection API is small and consistent:
| API | Meaning |
|---|---|
|
Array of the |
|
The class or module refined. Replaces |
|
Array of modules whose refinements are active at the call site, latest first. |
|
Array of the active |
|
See the previous section. |
|
For a refined method, the |
# frozen_string_literal: true
require "shelf"
title_case = Shelf::Refinements::TitleCase
p title_case.refinements
refinement = title_case.refinements.first
p refinement.class
p refinement.target
p refinement.instance_methods(false)
p refinement.ancestors.first(2)
p refinement.respond_to?(:refined_class)
p Module.used_modules
p Module.used_refinements
using Shelf::Refinements::TitleCase
p Module.used_modules
p Module.used_refinements
# respond_to? knows about refinements; the reflection that lists methods does not.
p "x".respond_to?(:title_case)
p String.method_defined?(:title_case)
p "x".methods.include?(:title_case)
p refinement.instance_method(:title_case).owner
[#<refinement:String@Shelf::Refinements::TitleCase>]
Refinement
String
[:title_case]
[#<refinement:String@Shelf::Refinements::TitleCase>]
false
[]
[]
[Shelf::Refinements::TitleCase]
[#<refinement:String@Shelf::Refinements::TitleCase>]
true
false
false
#<refinement:String@Shelf::Refinements::TitleCase>
Source: Module.used_modules
Module.used_modules and Module.used_refinements report the lexical state at the call site, so both print []
above the using line and the module and refinement below it. respond_to? honors refinements (true), while
method_defined? and methods do not (false). Refinement#inspect is #<refinement:Target@Module>.
Refinements in practice
A recommended shape
-
Define refinements in a module that says what it does (
Shelf::Refinements::TitleCase), one target perrefineblock, in a file of its own. -
Name the refinement module after its purpose, not the refined class, and document the added methods in the module’s comment, since documentation generators do not list refined methods under the refined class.
-
Activate with
usingat the top of the file that needs it, so the dependency is visible. Ausingin a file that is only loaded for its side effects ends with that file and activates nothing elsewhere. -
A library that uses a refinement internally keeps
usingin its own files; it does not leak to its users, which is the entire point. -
A library that offers a refinement to its users documents the exact
usingline and the Ruby version. -
Keep the refined methods small and prefer adding methods over replacing existing ones; a replacing refinement changes the behavior of code in the using scope that expects the original.
-
In a refinement block, a method replacing an existing one can reach the original with
super.
Testing a refinement
A test file activates the refinement exactly like production code would, with using at the top of the file (the refinement
is active until the end of the file, so it does not leak into other test files), and asserts the behavior on the
refined class. It is also worth testing that the core class is untouched.
# frozen_string_literal: true
require "minitest/autorun"
require "shelf"
# The test file activates the refinement, exactly like production code would.
using Shelf::Refinements::TitleCase
class TitleCaseTest < Minitest::Test
def test_capitalizes_each_word
assert_equal "The Left Hand Of Darkness", "the left hand of darkness".title_case
end
def test_core_class_is_untouched
refute String.method_defined?(:title_case)
end
def test_other_words_stay_untouched
assert_equal "Ico", "ICO".title_case
end
end
Run options: --seed 42
# Running:
...
Finished in 0.000207s, 14492.7492 runs/s, 14492.7492 assertions/s.
3 runs, 3 assertions, 0 failures, 0 errors, 0 skips
Source: Minitest
Run it with ruby -w -Ilib test.rb --seed 42. The seed makes the order reproducible; the timing line differs on each run.
Minitest and RSpec are covered in Testing with Minitest and
RSpec and Test Doubles. For a test that stubs a method on
a core class, use a stub or allow(...).to receive for the duration of one test instead of a global patch.
Performance and interoperability
-
Calls to refined methods take a different path in the VM than ordinary calls, so a hot loop may behave differently from the unrefined version. Measure before drawing conclusions, with the tools in Performance, Benchmarking and Profiling.
-
C code never sees a refinement, and neither does library code outside the using scope:
JSON.generate([1])ignores a refinedInteger#to_json, and aHashuses the originalhashandeql?. -
Tools that rely on
ObjectSpace,instance_methodsor documentation generators do not list refined methods. -
IRB accepts a top-level
usingtyped at the prompt, and the refinement stays active for the rest of the session (see IRB and Debugging).
Ruby::Box: isolation of definitions (experimental)
Ruby 4.0 adds Ruby::Box, an experimental in-process separation of classes and modules. The documentation’s description is
"separated spaces in a Ruby process, to isolate application codes, libraries and monkey patches". A box is a copy of the
definitions of the built-in classes; a monkey patch applied in one box is invisible in every other box. Ruby prints a warning
that the feature is experimental when it starts with it enabled (hide it with -W:no-experimental).
The feature is off by default. It must be enabled when the process starts by setting the environment variable
RUBY_BOX=1; setting it from inside the program does nothing. Without it, Ruby::Box.new raises RuntimeError
(Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box.). The example below must be run as
RUBY_BOX=1 ruby -W:no-experimental box_demo.rb.
# frozen_string_literal: true
# Ruby::Box is experimental in Ruby 4.0. Start the process with RUBY_BOX=1.
puts Ruby::Box.enabled?
box = Ruby::Box.new
box.eval(<<~RUBY)
class String
def title_case = split.map(&:capitalize).join(" ")
end
puts "in the box: " + "the dispossessed".title_case
RUBY
p "the dispossessed".respond_to?(:title_case)
puts Ruby::Box.current == Ruby::Box.main
p box.eval("Ruby::Box.current") == box
p box::String == String
true
in the box: The Dispossessed
false
true
true
true
Source: Ruby Box
What the output shows:
-
Ruby::Box.enabled?reports whetherRUBY_BOX=1was given. -
Ruby::Box.newcreates a user box;Box#eval,Box#require,Box#require_relativeandBox#loadrun code in it. -
The
Stringpatch defined inside the box (title_case) works there and is invisible outside: the main box answersfalse. -
Ruby::Box.currentis the box that runs the calling code; the main script runs in the "main" box. -
box::String == String: builtin classes are the same objects, with box-local definitions; a reference likebox::Stringis the String of the current box.
How it differs from refinements: a refinement is a lexical tool, controlled by using in the source, and it can be used
in one file of a program; a box is a process-level tool, controlled by where the code is loaded (a file runs in a single box), and everything defined by the loaded files, including constants, global variables and class variables, is
separated. The two are complementary. Constants, globals, top-level methods and monkey patches made in a box stay there.
The documentation lists known issues: native extensions may fail to install with RUBY_BOX=1, some libraries such as
active_support/core_ext may not work, and methods defined in a box may not be called by built-in methods written in Ruby (a
patch of Hash#each in a user box does not change Hash#map, which runs in the root box). Because of those limitations and
because the feature is experimental, the API may change; do not use it in production in 4.0. How boxes interact with require
and the load path is covered in Code Loading: require and autoload.
Choosing a technique
| Situation | Use | Why |
|---|---|---|
A helper for a few call sites |
A module function |
No global state, no surprises. |
Fluent methods on a core class in one library |
Refinement |
Private to the library; users are unaffected. |
Behavior change that every caller must see (a security fix, a backport) |
Prepended module or guarded polyfill |
Global but explicit and removable in the source. |
Wrap a method of a class you do not own |
|
|
A test double for one test |
A stub scoped to the test |
Reverts automatically. |
Separate two incompatible versions of the same patch in one process |
|
Process-level separation. |
An internal DSL that adds methods to blocks |
|
No core class touched (see Hooks, eval and DSLs). |
Where older books and tutorials are outdated
Books and tutorials on Ruby metaprogramming predate several changes that matter here. The points below correct the common ones; each is verified against the 4.0 documentation and the release notes.
| Older statement | Current behavior |
|---|---|
Refinements arrived in Ruby 2.0 |
They were experimental in 2.0 and became a stable feature in 2.1; the later honoring of |
Refinements are not honored by |
They are (see the table above). What is still unrefined is C-level calls such as |
A refinement may |
|
Use |
Prefer |
|
Removed in 3.4; use |
Introspection of refinements is not possible |
|
A monkey patch cannot be isolated from the rest of the process |
Ruby 4.0 adds the experimental |
Error messages like |
Ruby 3.3 and 3.4 print |
|
Since 3.4: |
Common exceptions and warnings
| Message | Cause |
|---|---|
|
The call is outside the using scope, or before the |
|
The same, for a bare name with an implicit receiver. |
|
|
|
|
|
|
|
The argument of |
|
|
|
|
|
|
|
|
|
An alias-chain patch was applied twice. |
|
|
|
Ruby started with |
RuboCop and tooling
-
Lint/RefinementImportMethods(RuboCop 1.27, pending by default, enabled byNewCops: enable) reportsincludeandprependinside arefineblock and suggestsimport_methods. -
Lint/DuplicateMethodsreports a method defined twice in the same class within the analyzed files: the static counterpart of Ruby’s runtime redefinition warning. -
Style/OneClassPerFilereports reopening classes in the same file (useful in a codebase that wants each patch in its own file). -
ruby -w(or$VERBOSE = true) prints themethod redefinedwarnings;Warning[:deprecated] = trueshows deprecated features such as chilled string mutation. -
Method#ownerandMethod#source_locationfind the origin of a method;Module#refinementsandModule.used_refinementslist refinements.
Pitfalls
-
Calling a refined method from a method defined above the
usingline: the method is outside the scope. -
Expecting
usingin a gem’slibfile to activate the refinement for the application. It does not leak, by design. -
Relying on
Array#join,putsora, b = objto see a refinedto_s,to_strorto_ary: the C implementations call the originals. -
Refining a method that
HashorSetcall (hash,eql?): the container uses the original and lookups misbehave. -
Putting
usinginside a method or anifbranch and expecting a conditional activation. -
Refining a superclass method that the subclass overrides: the subclass’s method wins.
-
Writing a patch with
alias_methodand loading it twice. -
Patching a core class in a test file and forgetting that the change leaks to the rest of the suite.
-
Reopening or creating a class by accident:
class Isbnat the top level creates a new::Isbninstead of reopeningShelf::Isbn, and Ruby reports no error. -
Using
Ruby::Boxas a stable security boundary: it is experimental and not a sandbox.
Version notes
| Version | Change |
|---|---|
2.0 |
|
2.1 |
Refinements stabilized. |
2.4 |
Refinements honored by |
2.5 |
Refinements honored in string interpolation. |
2.6 |
Refinements honored at block passing, |
2.7 |
Refinements honored by |
3.1 |
|
3.2 |
|
3.3 |
|
3.4 |
|
4.0 |
Experimental |
References
-
Refinement (
import_methods,target) -
Module#refine, Module#refinements, Module.used_modules, Module.used_refinements, Module#using and Module#prepend
-
Ruby command-line options (
-w,-W:no-experimental) -
The refinements specification (referenced by the official documentation)