
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.
Overview
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,
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
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.
Monkey Patches
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).
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
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
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.
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 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 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:
-
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.
Caveats
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.
Constants
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
Class Variables
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.
Methods
You cannot refine a method:
def example
using ExampleRefinements
end
The above will result in a NoMethodError
.
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
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
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
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 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
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>)
Main
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)
The RuntimeError
insures a refinement isn’t used at a global level because we’d be no better off
than when monkey patching code.
Benefits
Caveats aside, refinements still 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 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.
Guidelines
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,
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 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.
Implementations
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 Kernel
already provides for several Ruby primitives such as Integer
, String
, Array
, 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.
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.