Ruby Refinements: Scoped Monkey Patching
Refinements were introduced in Ruby 2.0 to solve one of the language’s most controversial features: open classes. They provide a way to modify classes locally without affecting the global namespace.
The Monkey Patching Problem
Ruby’s open classes are powerful but dangerous:
class String
def shout
self.upcase + "!!!"
end
end
"hello".shout # "HELLO!!!"
This modifies String globally for everyone. If two libraries both monkey-patch the same method, chaos ensues. Refinements provide scoped modification.
Basic Refinements
module StringExtensions
refine String do
def shout
self.upcase + "!!!"
end
end
end
# Without activation, the method doesn't exist
"hello".shout # NoMethodError
# Activate within a specific scope
using StringExtensions
"hello".shout # "HELLO!!!"
The key word is using. It activates refinements only within the current scope—a file or module/class body.
Scope Rules
Refinements have strict scoping:
module StringExtensions
refine String do
def shout = self.upcase + "!"
end
end
class MyClass
using StringExtensions
def test
"hello".shout # Works here
end
end
MyClass.new.test # "HELLO!"
"hello".shout # NoMethodError - not active here
The refinement is active:
- Inside the class/module that called
using - In methods defined after
using - Not in methods called from that context (!)
The Chain-Calling Gotcha
This is where refinements get tricky:
module StringExtensions
refine String do
def shout = self.upcase + "!"
end
end
class Processor
using StringExtensions
def process(str)
str.shout # Doesn't work!
end
def self.process_direct
"hello".shout # Works
end
end
Processor.process_direct # "HELLO!"
Processor.new.process("hi") # NoMethodError
Refinements don’t apply to method calls on arguments or variables. The receiver must be lexically present in the refined scope.
Practical Workarounds
To use refinements with passed objects, call the refined method explicitly:
module StringExtensions
refine String do
def shout = self.upcase + "!"
end
end
class Processor
using StringExtensions
def process(str)
# Call through a refined context
transform(str)
end
private
def transform(str)
str.shout # Works when str is used directly
end
end
Or embrace a functional style:
module StringExtensions
refine String do
def shout = self.upcase + "!"
end
using StringExtensions
def self.shout(str)
str.shout
end
end
StringExtensions.shout("hello") # "HELLO!"
When to Use Refinements
Refinements shine in specific scenarios:
DSL Enhancement
module DSLExtensions
refine Symbol do
def ~@
proc { |obj| obj.send(self) }
end
end
end
class Query
using DSLExtensions
# Now you can use ~:method_name
def select(&block)
# ...
end
end
Testing Extensions
module TestHelpers
refine String do
def to_user
User.find_by(name: self)
end
end
end
class UserTest < Minitest::Test
using TestHelpers
def test_something
user = "Alice".to_user
assert user.valid?
end
end
Library Internal Extensions
Refine within your library without polluting user space:
module MyGem
module CoreExtensions
refine Array do
def median
sorted = self.sort
len = sorted.length
(sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
end
end
end
class Calculator
using CoreExtensions
def calculate(numbers)
numbers.median
end
end
end
Limitations and Criticisms
Refinements remain controversial:
- Performance: Refinements can be slower than regular methods
- Complexity: The scoping rules confuse even experienced developers
- Limited adoption: Few libraries use them, reducing ecosystem benefit
- Chain-calling issues: The inability to refine passed objects frustrates many
Matz himself has acknowledged refinements didn’t fully solve the problems they targeted.
Alternatives
Before reaching for refinements, consider:
- Regular methods: Just write functions that take objects as arguments
- Delegators: Wrap objects instead of modifying them
- Prepend modules: If global modification is acceptable
- Method objects: Encapsulate behavior in classes
The Verdict
Refinements are a noble experiment in making Ruby safer without sacrificing flexibility. They work well for specific use cases—DSLs, testing, internal library extensions—but haven’t become the mainstream solution to monkey patching.
Understanding refinements helps you appreciate Ruby’s evolution and the tradeoffs involved in language design. They’re a tool in your toolbox; just be aware of their sharp edges.