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:

  1. Performance: Refinements can be slower than regular methods
  2. Complexity: The scoping rules confuse even experienced developers
  3. Limited adoption: Few libraries use them, reducing ecosystem benefit
  4. 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.