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.
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
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
"", so we extended
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
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.