Monkey Patching in Ruby
Ruby is a mighty fine language and my daily driver at work. Overall I've been pretty happy with its features, ecosystem and libraries for web development, however my previous dabbles into Python have left me longing for a yet unimplemented feature.
The problem
Consider the following pieces of code:
[][0]
{}[:ruby]
[][0]
{}['python']
A less experienced developer might expect both code snippets throwing
exceptions for the out of bounds and missing key access. This
assumption holds true for the Python variant, the Ruby variant however
happily returns nil
for both.
While one may argue that this behaviour can be convenient, I consider
it to be a potential source for the dreaded undefined method for
nil:NilClass
runtime error. The official recommendation is to use
the #fetch
method to raise exceptions for erroneous access on both
arrays and hashes. I could of course comply with that suggestion and
laboriously check where the convenience is safe to have and where not,
but what if I were to desire a feature reminiscent of JavaScript's use
strict
mode instead?
The solution
It turns out that it is surprisingly easy to make Ruby behave this way
thanks to its excellent metaprogramming facilities. The standard
method of achieving the desired result is opening
classes and
modifying their methods, but it's problematic as it is applied
globally and can therefore clash with code relying on the behaviour
(or other code monkeypatching the same classes in incompatible ways).
This is why I will instead try my hand at the
"Refinements" feature of the runtime which was introduced with Ruby
2.1.0. It allows one to change the behaviour of existing classes and
selectively enabling it for any scope. The other missing component of
this technical demonstration is alias_method
, a facility for adding
aliases to class methods. By aliasing the :fetch
to the :[]
method in a refinement, the desired effect can be obtained easily:
autoload :ItsATrap, 'its_a_trap'
module ItsATrap
refine Array do
alias_method :[], :fetch
end
refine Hash do
alias_method :[], :fetch
end
end
All that is left to do is packaging the module providing the refinement as a Ruby gem. The official guide recommends the following file structure:
.
├── its_a_trap.gemspec
└── lib
└── its_a_trap.rb
Here's the missing metadata file:
Gem::Specification.new do |s|
s.name = 'its_a_trap'
s.version = '0.0.0'
s.date = '2018-02-15'
s.summary = 'It\'s a trap!'
s.description = 'Strict mode for your Ruby files'
s.author = 'Vasilij Schneidermann'
s.email = 'v.schneidermann@gmail.com'
s.files = ['lib/its_a_trap.rb']
s.homepage = 'https://github.com/wasamasa/its_a_trap'
s.license = 'MIT'
end
gem build its_a_trap.gemspec; gem install its_a_trap-0.0.0.gem
will
then install the created gem locally, that way it can be used in your
own code like this:
require 'its_a_trap'
using ItsATrap
[][0] # throws IndexError
{}[:foobar] # throws KeyError
Monkeypatching in Ruby - Done!
And that's about it! Following these steps gives one a library that can be selectively enabled for debugging purposes and disabled for release candidates.