Refinements allow you to fix or enhance the behavior of an object but in a lexically scoped manner by keeping the change isolated to the file or object you are applying the refinement to. What makes Refinements unique is that they don’t persist changes, globally, as found when monkey patching.
To provide historical context, Refinements were added to the Ruby language as an experimental feature to Ruby in Version 2.0.0 and became fully supported in Version 2.1.0.
With the above in mind, let’s dive deeper into what refinements are, why they’re important, and how you can use them in your own code without introducing surprising behavior.
Benefits
At a high level, refinements shine because they:
-
Clearly define what is being refined by the
Module#refinemethod block. -
Clearly define who is using the refinement via the
Module#usingmethod. -
Use lexical scopes to ensure a refinement only applies to the file, module, or class that needs the refinement by not infecting other parts of the codebase.
-
Make it possible to modify legacy APIs or DSLs by refining only the specific behavior needed while not altering the rest of the codebase.
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 at the point of origin/definition, not 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.evalon a string of code.
Specifically, this means:
-
Different files get different top level scopes. For example,
a.rbis 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: primary.rb
module Primary
class Example
end
end
# File: secondary.rb
module Secondary
class Example
end
end
While the above defines the same Example class, each is structurally different because Example is unique due to being defined within different files and modules. If it helps, here’s another way to visualize the different scopes:
primary.rb -> Primary -> Example secondary.rb -> Secondary -> Example
If we build upon the above knowledge, we can now implement a refinement and see how lexical scope works:
module Refinements
refine String do
def to_bool = %w[true yes on t y 1].include? downcase.strip
end
end
Given the above string refinement, watch how behavior changes based on where the refinement is used:
module Primary
using Refinements
def self.demo = "yes".to_bool
end
module Secondary
def self.demo = "yes".to_bool
end
Primary.demo # true
Secondary.demo # undefined method `to_bool' for an instance of String (NoMethodError)
Since using Refinements was defined within the Primary module, only Primary.demo is refined but Secondary isn’t. We can fix behavior by using our refinement at the top of our file:
using Refinements
module Primary
def self.demo = "yes".to_bool
end
module Secondary
def self.demo = "yes".to_bool
end
Primary.demo # true
Secondary.demo # true
As you can see, we have no errors because our refinement is lexically scoped at the top of the file.
Usage
Implementing a refinement requires the use of two specific methods:
-
Module#refine -
Module#using
A few code examples are necessary to explain so we’ll 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.
Super
You can message super when refining a method to message the original, unrefined method. This is handy when needing to wrap and alter original functionality. Example:
module Refinements
refine Pathname do
def delete = super && self
end
end
With the above, the original functionality of Pathname#delete is preserved, due to messaging super, but we’ve added new functionality by answering self so you still have access to the pathname object despite the file being deleted.
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 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]
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 conversion function. The specifics of how this works 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:
Refinementis a class of theself(current context) insiderefinestatement. 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 by using an anonymous module. Consider the following:
class Demo
using(
Module.new do
refine Demo.singleton_class do
def example = "The `.#{__method__}` method was called."
end
end
)
def self.debug = puts example
end
Demo.debug # The `.example` method was called.
Demo.example # undefined method `example' for Demo:Class (NoMethodError)
As you can see, when messaging the .debug class method, it delegates to the refined .example class method which is public, by default, but behaves as a private method due to being temporarily available because of 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 = "The `.#{__method__}` method was called."
end
private_constant :Kit
def self.debug = puts Kit.example
end
Demo.debug # The `.example` method was called.
Demo::Kit.example # private constant Demo::Kit referenced (NameError)
In both code snippets, roughly, the same amount of code is used. The difference is due to lexical scope and the temporary nature of the refinement being used. You could argue that the refined implementation is more private than the implementation using a private constant.
Another use case is using anonymous refinements to refine functionality you have no way of knowing about head of time especially when using multiple inheritance via modules. Consider the following which combines method imports with anonymous inlines:
module Imports
def echo = __method__.upcase
end
module Methods
def echo = __method__
end
module Demo
extend Methods
end
using(Module.new { refine(Demo.singleton_class) { import_methods Imports } })
puts Demo.echo # "ECHO"
With the above, we have a way to dynamically refine functionality without knowing which module — in this case, the Demo module — will have the desired functionality ahead of time. Use of import_methods is only being used to keep the anonymous refinement as a one-liner.
You might be tempted to refine the Methods module but once the Methods module is extended within the Demo module we lose the ability to refine Methods and can only refine Demo but we can’t know which module Methods will be extended into ahead of time. By using an anonymous refinement, we can refine functionality after we know which module was extended (i.e. Demo). If you remove the line with the anonymous refinement (i.e. using), then you’ll see echo output instead of ECHO. Again, this use case is rare, but can be handy within your test suite.
To recap, while the above is interesting, there are some caveats to be aware of:
-
Anonymous refinements, syntactically, are not as elegant to read.
-
Dynamically creating a refinement is costly in terms of performance and you don’t want to repeatedly
refinean object due to being an expensive operation. Definitely check out the refinements section of my Benchmarks project to learn more.
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>
Target
Along the lines of the above — and once you have a refinement in hand — you can ask the refinement for its target class. Example:
module Demo
refine(Integer) { def moniker = "Two" }
refine(String) { def moniker = "One" }
end
Demo.refinements.map(&:target) # [Integer, String]
As you can see, this allows you to know what the current refinement is refining. In this case, an Integer and a String.
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:
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:
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,
Refinesis the module I use so my custom implementation can peacefully coexist with functionality provided by the Refinements gem without having to use rooted::Refinementssyntax 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
usingthroughout 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. -
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
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
Versionobject — which adheres to Strict Semantic 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
You’ve learned the multi-facited aspects of refinements. I hope this encourages you to leverage refinements instead of obscuring implementation details within a monkey patch. Refinements provide a way to be transparent while self-describing. Finally, not every object needs be refined, but when new behavior is applied appropriately, a refinement can be all that is needed to bring extra joy to your codebase.