Tailor your monkeys with refinements
One of the most powerful features of Ruby is open classes. You can reopen any class and change, or extend it. Unfortunately these monkey patches have global scope, so their convenience comes with possible side effects. In Ruby 2.1+ refinements allow you to limit the scope of monkey patches, so you can enjoy their convenience again.
blank?
Ruby on Rails brought many monkey patches into mainstream use and they’ve become second nature. For example, ActiveSupport provides a #blank?
method on almost every object by monkey patching several standard library classes:
Object#blank? #=> [true, false]
Array#blank? #=> [true, false] alias of Array#empty?
Hash#blank? #=> [true, false] alias of Hash#empty?
String#blank? #=> [true, false] using a regular expression to detect empty and whitespace only strings
NilClass#blank? #=> true
FalseClass#blank? #=> true
TrueClass#blank? #=> false
Numeric#blank? #=> false
Read the source code in ActiveSupport 4.2.0.
What’s your favourite monkey patch in Rails? Maybe it’s "octopus".pluralize
, or 3.months.ago
?
Rolling your own
#blank?
is so handy that we missed it when we started writing a new Ruby app at globaldev. Because we want to avoid dependencies whenever possible, we turned to refinements to roll our own version, tailored to our needs.
In this case, we wanted a convenient syntax for checking parameters in a Rack app. To handle missing or empty parameters we had vanilla Ruby code like:
member_id = String(request.params["member_id"])
if member_id.empty?
# return 400 Bad Request
end
For our use case we only needed to handle nil
and ""
, so we extended NilClass
and String
using refinements:
module Blank
refine String do
alias_method :blank?, :empty?
end
refine NilClass do
def blank?
true
end
end
end
You can group several refinements in a single module and activate them together in your code. With this refinement we could reduce the boilerplate code for handling parameters to:
using Blank
if request.params["member_id"].blank?
# return 400 Bad Request
end
Scope
The purpose of refinements is to limit the scope of monkey patches, which should make them easier to reason about and avoid unintended consequences. In addition, refinements must be activated explicitly, which makes it clear when they are active.
The scope of refinements is per file and lexical. You can activate them at the top-level of a file and they will be activated until the end of the file. Or you can activate them inside a class or module and they will be activated until the end of the class or module definition.
Here is an example file showing how our #blank?
refinements can be scoped to a single class definition.
require_relative "blank"
class App
using Blank
# activated here
def call(env)
# here
end
# and here
end
# but not here
Read the documentation on ruby-doc.org.
With refinements we can enjoy the wonderful convenience of monkey patching Ruby with less potential for bugs. Enjoy!
P.S. If you’d like to see another example in action. Check out my latest implementation of FizzBuzz using Enumerator and a refinement of Integer.