/ Tags: RUBY 3 / Categories: RUBY

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 + using inside 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.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More