
Refinements were added to the Ruby language as an experimental feature to safely modify an object’s methods in an isolated manner in Version 2.0.0 and became fully supported in Version 2.1.0. In this context, to modify means changing existing behavior, adding new behavior, or disabling behavior altogether. What makes Refinements unique is that they don’t persist changes, globally, as found when monkey patching. The goal of this article is to dive deeper into what refinements are, why they’re important, and how to use them.
Benefits
At a high level — and before we get into the details — refinements shine because they:
-
Clearly define what is being refined by the
Module#refine
method block. -
Clearly define who is using the refinement via the
Module#using
method. -
Use lexical scopes to ensure a refinement only applies to the file, module, or class that needs the refinement and doesn’t bleed into any other part of a program accidentally.
-
Make it possible to modify legacy APIs or DSLs by refining only the specific behavior needed while not altering the rest of the program.
The rest of this article will dive deeper into explaining the value of these benefits.
Background
Before diving further into refinements, it’s important to take a step back and discuss how they rose out of a need to avoid monkey patches by leveraging lexical scopes.
Monkey Patches
Let’s start with monkey patches, what they are, and why they are not recommended. Here’s the Wikipedia definition:
A monkey patch is a way for a program to extend or modify supporting system software locally (affecting only the running instance of the program).
For those of you who have worked with Smalltalk, you’ll know this as Duck Typing:
If it walks like a duck and it quacks like a duck, then it must be a duck.
💡 In fact, the Ruby language has many influences, including languages such as Perl, Eiffel, Ada, Basic, and Lisp, but especially Smalltalk from which monkey patching or duck typing is inherited.
Again, the problem with monkey patches is that, once applied, change the application globally. To illustrate, let’s study a simple monkey patch:
class String
def split(pattern = nil) = fail StandardError, "Sorry, I have no idea how to split myself. 😈"
end
In the above, we monkey patch the String
class by opening it to add new behavior. Unfortunately,
this new behavior now applies to all strings within the application that need to split themselves.
Here’s the result of this change (truncated for brevity):
"example".split # => Sorry, I have no idea how to split myself. 😈 (StandardError)
With the above example, we at least have a stack dump, despite not being shown, to trace back to where the monkey patch was introduced. What if the change was more subtle?
class String
def size = 666
end
Upon using the above monkey patch, we see the following output:
"example".size # => 666
Unfortunately, String#size
always answers 666
now. Imagine if this code wasn’t in our
application but was loaded from a gem dependency or — worse — indirectly via a gem’s own
dependency. Beyond the struggle of identifying the root cause, the above also breaks expected
behavior understood by all Ruby engineers, leading to time lost debugging confusing behavior not
seen before.
💡 To bolster my argument further, consider Piotr Solnica’s article on why Rails is not written in Ruby where he explains the implications of taking monkey patches too far.
We can avoid monkey patches by leveraging refinements because any object using a refinement is
transparently declared via the Module#using
method. To understand the power of refinements and
just how isolated the behavior is that they introduce, we need to talk about lexical scopes next.
Lexical Scopes
Lexical scope is defined from the point of origin/definition, not from where the code is used elsewhere in the program/application. This means lexical scope occurs when:
-
Entering a different file.
-
Opening a class or module definition.
-
Executing
Kernel.eval
on a string of code.
Specifically, this means:
-
Different files get different top level scopes. For example,
a.rb
is not the same scope asb.rb
. -
Scope hierarchy is not the same as class hierarchy.
-
Refinements are activated within current and nested scopes only.
-
Methods and blocks are evaluated using the lexical scope of their definition.
Take the following, for example:
# File: primaries.rb
module Primaries
class Example
end
end
# File: secondaries.rb
module Secondaries
class Example
end
end
While the above defines the same Example
class, each is different structurally. They’re within
different files and modules. Here’s another way to visualize the different scopes:
primaries.rb -> Primaries -> Example secondies.rb -> Secondaries -> Example
Within the same file, scope can change based on where the refinement is defined:
using Refinements
module Primaries
class Example
end
end
module Secondaries
class Example
end
end
With the above, Refinements
is defined at the top of the file and so is available to both the
Primaries::Example
and Secondaries::Example
classes. However, if Refinements
were to be
defined after Primaries
, then only the Secondaries
would be able to use it:
module Primaries
class Example
end
end
using Refinements
module Secondaries
class Example
end
end
To learn more, I’d suggest the following articles:
-
Refinements as provided by official Ruby Documentation.
-
Bindings and Lexical Scope in Ruby by Jeff Kreeftmeijer.
-
Understanding Ruby Refinements and Lexical Scope by Starr Horne.
-
Lexical scoping and Ruby class variables by Starr Horne.
Usage
Implementing a refinement requires the use of two specific methods:
A few code examples are necessary to explain so we will start by refining instance methods, class methods, and then follow-up with more advanced usage.
Instance Methods
If we’d like to teach strings to answer if they are boolean or not, for example, here’s how we could implement that behavior by adding a new instance method:
module Refinements
refine String do
def to_bool = %w[true yes on t y 1].include?(downcase.strip)
end
end
The first and only argument of the refine
block requires a class or module. In the implementation,
above, String
is passed as an argument to refine
. Next, we use the newly minted refinement,
via the using
method, within the object that needs the behavior:
class Example
using Refinements
def self.boolean?(string) = string.to_bool
end
Finally, we can message our refined Example
object as follows:
Example.boolean? "on" # => true
Example.boolean? "off" # => false
💡 If you like this refinement, you can get this behavior and more via the Refinements gem.
Because refinements are lexically scoped, we’ll not see this behavior exhibited in different objects which are not using the refinement:
class OtherExample
def self.boolean?(string) = string.to_bool
end
OtherExample.boolean? "true" # => NoMethodError (undefined method `to_bool' for "true":String)
Now that we understand how to refine instance methods, let’s continue by looking at class methods next.
Class Methods
Building upon the previous instance method discussion, you can also refine a class. The difference is making sure you refine the singleton class. Here’s how you would refine String
to answer if another string was a boolean or not:
module Refinements
refine String.singleton_class do
def boolean?(string) = %w[true yes on t y 1].include?(string.downcase.strip)
end
end
using Refinements
String.boolean? "yes" # true
String.boolean? "no" # false
⚠️ As with any class method, refined or not, use sparingly because implementing too many of them means you have an object wanting to be born which would decrease the complexity of the class.
Main
As briefly shown with class methods, a refinement is meant to be used within a module or class. That said, you can refine main
(new as of Ruby 3.1.0) which is handy when using IRB for exploration purposes. For example, try pasting the following in IRB:
module Refinements
refine String do
def to_bool = %w[true yes on t y 1].include? downcase.strip
end
end
using Refinements
"yes".to_bool # true
This also makes gem development easier for projects like the Refinements gem where you can run the console
and safely experiment with all refinements within the project specific IRB console.
Conversion Functions
Conversion functions — also known as casting functions — are a unique aspect of Ruby that allow you to cast any object into a primitive or raise an exception otherwise. Here’s a quick example of a few of these functions in action:
Integer "1" # 1
String 1 # "1"
Hash a: 1, b: 2 # {:a=>1, :b=>2}
Array 1..3 # [1, 2, 3]
So what happens when you want to introduce a new primitive into the language? Well, you could monkey patch Kernel
by adding your own conversion function but we already know that’s not acceptable. The good news is refinements are especially suited for solving this problem. The best example is via the Versionaire gem which provides a Version
casting function. The specifics of how this work is documented within the Versionaire Refinement documentation but here’s a code snippet showcasing how nice it is to refine Kernel
by adding a new Version
function:
using Versionaire::Cast
Version "1.0.0"
Version [1, 0, 0]
Version major: 1, minor: 0, patch: 0
All of the above answer the same #<struct Versionaire::Version major=1, minor=0, patch=0>
object. Without the refinement, you’d have to type Versionaire::Version
all the time but with the refinement you only have to type Version
just like you would with native conversion functions such as Integer
, String
, Array
, and so forth.
Method Imports
With Ruby 3.1.0, support for importing methods into a refinement was introduced but only for pure Ruby objects. Native extensions, methods defined in C, and so forth can’t be imported because only the bytecodes of pure Ruby methods are allowed.
Defining functionality once and then importing that shared functionality across several refined
objects reduces duplicated effort. For example, consider the following module which implements the
#many?
method for an enumerable:
module Many
def many?
return size > 1 unless block_given?
total = reduce(0) { |count, item| yield(item) ? count + 1 : count }
total > 1
end
end
If we import the above implementation into the Array
and Hash
objects, we then benefit from
shared functionality defined only once:
refine Array do
import_methods Many
end
refine Hash do
import_methods Many
end
This is made possible by using the Refinement#import_methods
method which copies the module’s
bytecodes into the refined object. The #import_method
can also take a comma separated list of
modules to import as well.
All of what has been described here is, roughly, how the #many?
method was implemented for the
Array
and Hash
objects in the Refinements gem. So you can make use of this today in your
own code if you like.
Anonymous Inlines
You can inline a refinement within the same class it is defined by using an anonymous module. Consider the following:
class Demo
using(
Module.new do
refine Demo.singleton_class do
def example = "You've called the .#{__method__} method."
end
end
)
def inspect = self.class.example
end
Demo.new.inspect # You've called the .example method.
Demo.example # undefined method `example' for Demo:Class (NoMethodError)
As you can see from the above, when you message the #inspect
instance method, it delegates to the .example
class method which is public by default but ends up being private in nature because it’s only temporarily available due to the refinement’s lexical scope. However, if you message the .example
class method directly, you end up with a NoMethodError
because, again, lexical scope to the rescue.
The alternative would be use a private constant. Here’s the same implementation but rewritten:
class Demo
module Kit
def self.example = "You've called the .#{__method__} method."
end
private_constant :Kit
def inspect = Kit.example
end
Demo.new.inspect # You've called the .example method.
Demo::Kit.example # private constant Demo::Kit referenced (NameError)
In both code snippets, roughly, the same amount of code is used. The difference is that due the lexical scope and temporary nature of the refinement being used, you could argue that the implementation using the refinement is more private than the private constant implementation.
Whether this is a good use case for refinements, I can’t say. There might be performance, debugability, readability, and other concerns. That said, being able to use a private class method as a utility method is an interesting use case that refinements handle uniquely well.
Caveats
As powerful as refinements are, there are a few caveats to be aware of before using them in your own work. These caveats are mostly restrictions implied by lexical scope — which is a good thing — but are good to be aware none-the-less.
Constants
You cannot refine a constant at the class or instance level. For example, the following code is
invalid because it will yield an undefined method `refine' for main:Object
error:
# Class Refinement
refine String.singleton_class do
EXAMPLE = "example"
end
# Instance Refinement
refine String do
EXAMPLE = "example"
end
Class Variables
As with constants, you cannot refine a class variable either as you’ll get a undefined method `refine' for main:Object
error:
refine String
@@example = "demo"
end
By the way, if you are using class variables, please don’t. Stop now. They are an antipattern and should be avoided altogether.
Methods
You cannot refine a method because the following will result in a Module#using is not permitted in methods
runtime error.
module ExampleRefinements
refine String do
def inspect = "This is a test."
end
end
module Demo
def self.test
using ExampleRefinements
"test".inspect
end
end
Demo.test
Introspection
Currently, method introspection via Kernel#method
, Kernel#methods
, Object#respond_to?
, and
Object#public_send
is not possible. For example, study the output below when example
is
messaged:
module Refinements
refine String do
def example_refined = "A refined method."
end
end
class Example
using Refinements
def example_normal = "A normal method."
end
Example.new.then do |example|
p example.method(:example_refined).source_location
# => `method': undefined method `example_refined' for class `Example' (NameError)
p example.methods.grep(/example/)
# => [:example_normal]
p example.respond_to?(:example_refined)
# => false
p example.public_send(:example_refined)
# => `public_send': undefined method `example_refined' for #<Example:0x00007fa7bf585390> (NoMethodError)
end
Message Chains
Attempting to send the same, refined, message to the object answered back will result in a
NoMethodError
:
require "pathname"
module Refinements
refine Pathname do
def name = basename(extname)
end
end
class Example
using Refinements
def self.to_name(path) = Pathname(path).name
end
p Example.to_name "/tmp/example.txt"
# => #<Pathname:example>
p Example.to_name("/tmp/example.txt").name
# => NoMethodError (undefined method `name' for #<Pathname:example>)
The error above occurs because only Example.to_name
is using Refinements
. When Example.to_name
answers back the new Pathname
object, we can’t send the #name
message because it’s not using the
refinement.
Monkey Patches
While monkey patches should be avoided, attempting to monkey patch a refinement will void the refinement due to introducing a new lexical scope. For example, look at the output below:
require "pathname"
module Refinements
refine Pathname do
def(name) = basename(extname)
end
end
class Example
using Refinements
def self.to_name(path) = Pathname(path).name
end
class Example
def self.to_name(path) = Pathname(path).name
end
p Example.to_name "/tmp/example.txt"
# => NoMethodError (undefined method `name' for #<Pathname:/tmp/example.txt>)
Guidelines
Now that you understand the benefits of refinements, versus monkey patches, here are a few guidelines to help you leverage refinements in the best possible way:
-
When adding refinements to your gem, program, and/or application, use a clearly defined module for namespacing these refinements. For example,
Refinements
is the module I tend to use. Whatever you chose, be consistent. -
When laying out the file structure, stick with
lib/refinements
for gems andapp/refinements
for frameworks like Hanami, Ruby on Rails, etc. This makes refinements more prominent and easy to find by fellow engineers. -
While you can refine class methods, as shown earlier via
refine String.singleton_class
, it’s best to be judicious in their use. -
When implementing refinements, define them within a single module of a single file for easier organization. For example, especially within a gem, check out the Refinements gem.
-
As with all tools, use refinements sparingly. Don’t lose sight of the fact that Ruby is an object oriented language. If you don’t have to modify a core Ruby object, parts of an existing gem, etc. then don’t. Sometimes constructing objects that interact with existing code is a better choice. In situations where minor modifications can be made, then Refinements might be the exact syntactic sugar you need.
Implementations
As you’ve probably guessed by now, I’m a proponent of using refinements to add syntactic elegance to the Ruby language when appropriate. I’ve even crafted several gems that refine Ruby even further. Here’s where you can start digging deeper, if you like:
-
Refinements - The gem — that I’ve mentioned a couple of times in the course of this article — which enhances and extends core Ruby functionality.
-
Versionaire - This gem provides a primitive
Version
object — which adheres to Semenatic Versioning — and is made possible by refiningKernel
. -
Projects - You can always browse through any of my open source projects. Many are built upon the Refinements gem and provide plenty of usage examples for solving common problems within your own work.
Conclusion
I hope this article has shed more light on refinements and encourages you leverage them instead of obscuring implementation details within a monkey patch. Refinements provide a way to be transparent while also self-describing. Finally, remember that not every object needs be modified but, when new behavior is applied appropriately, it can be all that is needed to bring extra joy to your codebase.