Refinements in Ruby — Scoped Monkey Patching Without the Footguns
Monkey patching in Ruby has a reputation — usually deserved — for causing exactly the kind of spooky action-at-a-distance bugs that make senior engineers twitch. Open a class anywhere in the codebase, add a method, and every object of that class everywhere changes behavior. Refinements are Ruby’s answer to this: a way to extend existing classes within a controlled lexical scope, so your changes only apply where you explicitly activate them.
What Refinements Are
A refinement is a modification to a class that only takes effect in files or modules where you call using. Outside that scope, the class behaves as if the refinement was never defined.
Example:
module StringExtensions
refine String do
def palindrome?
self == self.reverse
end
end
end
# Without `using` — method doesn't exist
"racecar".palindrome? # => NoMethodError
# With `using` — method exists in this file's scope
using StringExtensions
"racecar".palindrome? # => true
"hello".palindrome? # => false
The key difference from open-class patching: palindrome? doesn’t exist anywhere else in the application. Other files, other gems, other threads — they never see it.
Defining Refinements
Refinements live inside a module, using the refine method to specify which class you’re extending.
Example:
module IntegerExtensions
refine Integer do
def factorial
return 1 if self <= 1
self * (self - 1).factorial
end
def times_map(&block)
Array.new(self, &block)
end
end
end
module ArrayExtensions
refine Array do
def second
self[1]
end
def average
return 0.0 if empty?
sum.to_f / size
end
end
end
You can define multiple refinements in a single module, and you can have refinements for multiple classes.
Example:
using IntegerExtensions
using ArrayExtensions
5.factorial # => 120
3.times_map { |i| i * 2 } # => [0, 2, 4]
[10, 20, 30].second # => 20
[10, 20, 30].average # => 20.0
Scope Rules — Where using Takes Effect
The scope of a refinement is lexical, not dynamic. This is the most important thing to understand about refinements. The scope is determined at parse time based on where using appears in the source file.
Example:
using IntegerExtensions
def calculate(n)
n.factorial # Works — refinement is active in this file's scope
end
class Calculator
using ArrayExtensions # Also works — activates at class scope
def average(nums)
nums.average
end
end
Example:
# What refinements cannot do — dynamic activation
def activate_refinements
using IntegerExtensions # RuntimeError: refinement used in non-main code
end
# `using` must appear at top level or class/module level, not inside methods
The runtime error on dynamic using is intentional — it keeps refinement scope predictable. If you could activate refinements dynamically, callers couldn’t reason about what methods are available.
Refining Methods vs Adding Methods
You can use refinements to both add new methods and override existing behavior.
Example:
module SafeDivision
refine Integer do
def /(other)
return Float::INFINITY if other == 0
super
end
end
end
# Without refinement
10 / 0 # => ZeroDivisionError
# With refinement
using SafeDivision
10 / 0 # => Infinity
10 / 2 # => 5 (super delegates to original /)
super inside a refinement calls the original method — the method that would have been called without the refinement active. This makes overriding safe: you can add behavior without completely replacing the original implementation.
Real-World Use Cases
1. Domain-Specific String Formatting
module ReportFormatting
refine Float do
def to_currency(symbol = "$")
"#{symbol}#{"%.2f" % self}"
end
def to_percentage
"#{"%.1f" % (self * 100)}%"
end
end
refine Integer do
def to_currency(symbol = "$")
to_f.to_currency(symbol)
end
end
end
module Reports
using ReportFormatting
def self.summary(revenue, margin)
"Revenue: #{revenue.to_currency} Margin: #{margin.to_percentage}"
end
end
Reports.summary(49_999.5, 0.237)
# => "Revenue: $49999.50 Margin: 23.7%"
2. Test Helpers Without Polluting Production Classes
module TestHelpers
refine Array do
def include_hash_matching?(expected)
any? { |item| expected.all? { |k, v| item[k] == v } }
end
end
end
RSpec.describe UserService do
using TestHelpers
it "returns users with matching attributes" do
result = UserService.all
expect(result).to include_hash_matching?(role: "admin", active: true)
end
end
3. Nil-Safe Method Chaining
module NilSafe
refine NilClass do
def to_s = ""
def to_i = 0
def to_a = []
def fetch(*) = nil
end
end
using NilSafe
user = nil
user.to_s # => "" instead of "" (already works, but)
user.fetch(:name) # => nil instead of NoMethodError
Gotchas and Limitations
Refinements come with real restrictions that exist to preserve the lexical scope guarantee.
Example:
module StringExtensions
refine String do
def shout
upcase + "!!!"
end
end
end
using StringExtensions
# Works in current lexical scope
"hello".shout # => "HELLO!!!"
# Does NOT work via send or dynamic dispatch
"hello".send(:shout) # => NoMethodError — refinements aren't visible to send
# Does NOT work in eval'd strings
eval('"hello".shout') # => NoMethodError
Example:
# Refinements do not affect method_missing
# Refinements are not inherited — subclasses don't get them automatically
class MyString < String; end
using StringExtensions
MyString.new("hello").shout # Works — but only because String is refined
# and MyString inherits from String
The send limitation catches people off guard. If your code relies on dynamic dispatch, refinements won’t work there. That’s by design — dynamic dispatch breaks the lexical scope guarantee.
Pro-Tip: Refinements shine brightest in two places: gem internals (where you need to extend core classes without affecting users of the gem) and test files (where you want test-only convenience methods without any risk of them leaking into production code). If you’re writing a gem that needs to add methods to core classes,
refine+usinginside the gem’s own files is the responsible choice. Your users never see those extensions, and other gems never conflict with them.
Conclusion
Refinements offer a controlled alternative to open-class patching. They’re not a replacement for well-designed abstractions, and they come with real constraints around dynamic dispatch and scope. But in the cases where you genuinely need to extend a class — domain formatting, test helpers, gem internals — refinements let you do it without leaving footguns behind. The lexical scope requirement that initially feels restrictive is actually what makes them safe to use.
FAQs
Q1: Are refinements available in all Ruby versions?
Refinements were introduced in Ruby 2.0 but marked experimental until Ruby 2.4. They’re stable and fully supported in Ruby 3.x. The core behavior hasn’t changed significantly since 2.4.
Q2: Can I use a refinement across multiple files?
Yes. Define the refinement module once, then using ModuleName in each file where you want it active. Each file’s activation is independent and lexically scoped to that file.
Q3: Do refinements affect performance?
There’s a small overhead at method dispatch in files that use refinements, because Ruby needs to check if a refinement applies. In practice, this overhead is negligible for typical application code. It becomes relevant only in extremely hot loops.
Q4: Can I refine my own classes or only core Ruby classes?
You can refine any class, including your own. Refinements are most useful for core classes because that’s where monkey patching risks are highest, but the mechanism works for any class.
Q5: Why can’t using be called inside a method?
Allowing using inside methods would make refinement scope dynamic — whether a method exists would depend on which runtime code paths had executed. The restriction to lexical (parse-time) scope is what makes refinements predictable and safe to reason about.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀