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.
Implementing a refinement requires the use of two specific methods:
A few code examples are necessary to explain. 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:
module Refinements refine String do def to_bool %w[true yes on t y 1].include? downcase.strip end end end
The first and only argument of the
refine block requires a class or module. In the implementation,
String is passed as an argument to
refine. Next, we use the newly minted refinement,
using method, within the object that needs the behavior:
class Example using Refinements def self.boolean? string string.to_bool end end
Finally, we can message our refined
Example object as follows:
Example.boolean? "on" # => true Example.boolean? "off" # => false
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 end
We’ll discuss lexical scopes later. Returning to the above, here is what is yielded:
OtherExample.boolean? "true" # => NoMethodError (undefined method `to_bool' for "true":String)
💡 If you like the refinement above, you can get that behavior and more via the Refinements gem.
Before diving further into refinements, it’s important to take a step back and talk about monkey patches. Here’s the Wikipedia definition of this practice:
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).
If it walks like a duck and it quacks like a duck, then it must be a duck.
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 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 end
Upon using the above monkey patch, we see the following output:
"example".size # => 666
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
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.
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.
Kernel.evalon a string of code.
Specifically, this means:
Different files get different top level scopes. For example,
a.rbis not the same scope as
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
Secondaries::Example classes. However, if
Refinements were to be
defined after the
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:
As powerful as refinements are, there are a few caveats to be aware of before implementing refinements in your own work. These caveats are mostly restrictions implied by lexical scope.
You cannot refine a constant at the class or instance level. For example, the following code is
invalid as it will yield the
not defined at the refinement, but at the outer class/module warning:
# Class Refinement refine String.singleton_class do EXAMPLE = "example" end # Instance Refinement refine String do EXAMPLE = "example" end
As with constants, you cannot refine a class variable either as you’ll get a
not defined at the
refinement, but at the outer class/module warning:
refine String @@example = "demo" end
By the way, if you are using class variables, please don’t. Just stop now. They are a code smell and should be avoided altogether.
You cannot refine a method:
def example using ExampleRefinements end
The above will result in a
Currently, method introspection via
Object#public_send is not possible. For example, study the output below when
module Refinements refine String do def example_refined "A refined method." end end end class Example using Refinements def example_normal "A normal method." end 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
Attempting to send the same, refined, message to the object answered back will result in a
require "pathname" module Refinements refine Pathname do def name basename extname end end end class Example using Refinements def self.to_name path Pathname(path).name end 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
answers back the new
Pathname object, we can’t send the
#name message because it’s not using the
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 end class Example using Refinements def self.to_name path Pathname(path).name end end class Example def self.to_name path Pathname(path).name end end p Example.to_name "/tmp/example.txt" # => NoMethodError (undefined method `name' for #<Pathname:/tmp/example.txt>)
As mentioned before, a refinement must be used within a module or class. To illustrate what this means, try pasting the following in an IRB console:
module Refinements refine String do def to_bool %w[true yes on t y 1].include? downcase.strip end end end using Refinements
Running the above, will result in the following error:
RuntimeError (main.using is permitted only at toplevel)
RuntimeError insures a refinement isn’t used at a global level because we’d be no better off
than when monkey patching code.
Caveats aside, refinements still shine because they:
Clearly define what is being refined by the
Clearly define who is using the refinement via the
Use lexical scopes to ensure the 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.
Now that you understand the benefits of refinements, especially instead of 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,
Refinementsis the module I tend to use. Whatever you chose, be consistent.
When laying out the file structure, stick with
lib/refinementsfor gems and
app/refinementsfor 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 examples, 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.
As hinted earlier, I’m a proponent of using refinements to add syntactic elegance to the Ruby language when appropriate. I even crafted a gem for this called: Refinements. Perhaps not the most inventive name, but it is pragmatic, at least!
Should the Refinements gem not be convincing enough, then I would
encourage you to study the Versionaire gem. Versionaire is unique in
that it refines
Kernel so you can have a native
Version conversion function much like
already provides for several Ruby primitives such as
Hash, etc. Being
able to refine
Kernel with a custom type while avoiding unexpected behavior from polluting your
entire code base is a huge win and powerful use of a refinement.
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.