
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, 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 infect other parts 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
To understand monkey patches, what they are, and why they are best avoided, I high recommend reading the section on monkey patches in my Ruby Antipatterns article for a detailed explanation. In a nutshell, monkey patches are hard to debug and maintain over time.
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 Scope
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
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
Added in 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 duplication. For example, consider the following module which implements the #many?
method for an enumerable (as taken from the Refinements gem):
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. By the way, the #import_method
can also take a comma separated list of modules to import as well. To quote from the Ruby documentation:
Refinement
is a class of theself
(current context) insiderefine
statement. It allows to import methods from other modules.
To illustrate further, consider this simple example:
module Demo
def self.examine = refine(String) { def echo(text) = text }
end
refinement = Demo.examine
refinement # #<refinement:String@Demo>
refinement.class # Refinement
refinement.ancestors # [#<refinement:String@Demo>]
When messaging Demo.examine
, you can see an anonymous class is answered. This is further illustrated by inspecting the ancestors. In addition, we all see the anonymous class is a Refinement
which finally explains why we are able to use #import_methods
.
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.
Introspection
With the release of Ruby 3.2.0, several introspection enhancements were added which are documented in more detail below.
Used
You can dynamically obtain a list of modules using refinements for the current scope. Example:
module One
refine(Object) { def moniker = "One" }
end
module Two
refine(Object) { def moniker = "Two" }
end
using One
Module.used_refinements # #<refinement:Object@One>
using Two
Module.used_refinements # #<refinement:Object@One>, #<refinement:Object@Two>
This is similar to how Module.used_modules
works except specifically for refinements.
Refinements
Similar to the above, you can do the same by asking the receiver for any/all refinements defined within it.
module Demo
refine(Integer) { def moniker = "Two" }
refine(String) { def moniker = "One" }
end
Demo.refinements # #<refinement:Integer@Demo>, #<refinement:String@Demo>
Refined Class
Along the lines of the above — and once you have a refinement in hand — you can ask the refinement for it’s refined class. Example:
module Demo
refine(Integer) { def moniker = "A Refined Integer" }
end
Demo.refinements.first.refined_class # Integer
As you can see, this allows you to know what the current refinement is refining. In this case, an Integer
.
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. They are an antipattern and should be avoided.
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 Object#method
, Object#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 = Example.new
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)
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 custom refinements to your implementation, use a clearly defined module for namespacing these refinements. For example,
Refines
is the module I use so my custom implementation can peacefully coexist with functionality provided by the Refinements gem without having to use rooted::Refinements
syntax everywhere. Whatever you chose, be consistent. -
When laying out your custom file structure, stick with
lib/refines
. This makes your custom refinements prominent and quick to find by fellow engineers. -
While you can refine class methods — as shown earlier via
refine String.singleton_class
— be judicious in use. -
When implementing refinements, define them within a single module of a single file for easier organization. Check out the Refinements gem for further examples.
-
Avoid heavy use of
using
throughout your code base since this will cache bust your classes which can lead to slow boot times. You’ll also want to avoid refining (i.e.refine
) heavily used methods for the same reason. That said, this is mostly a concern for massive monolith applications. There is currently an open issue related to a Ruby 3.0.0 performance regression which hopefully will be fixed soon. -
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 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.