Notes On Sane Monkey-patching
Here are (semi)random notes, trying to outline a system of views on controversional “monkey-patching” topic—the views I myself consider sane and reasonable. It may seem other way for you, but it could be a start for rational discussion.
Monkey-what?..
In below text, we’ll use term “monkey patch” for changing behavior of some Ruby classes and modules you haven’t written, whether it be core classes, or standard library ones, or classes of third-party library, gem, framework.
Fun fact I can’t validate: “monkey patch” ← “gorilla patch” ← “guerilla patch”. This theory of etimology is reasonable, but for me it looks more like an “urban myth” (or backwards ethymology).
At good ol’ days, an ability to “patch” any class and library behavior was considered Ruby’s strength, but lately (and I mean, like, “5+ years” lately) it was more and more “considered harmful” by thought leaders.
But let’s take it slowly.
Why do we monkey-patch?
Some (real and artificial) examples.
1. To have “more pretty/natural/readable” code
five_days_ahead = Time.now + 60 * 60 * 24 * 5 # Fooooo
five_days_ahead = Time.now + 5.days # Cool!
The (well-known) ActiveSupport’s code above requires just defining some
methods in Ruby’s Numeric
class, like day
/days
, and voila! You,
kinda, have your code as close to just saying “5 days” as it is possible
staying inside Ruby.
2. To interweave existing and new code
ActiveSupport’s example above also requires patching Time
class:
5.days
statement, in fact, returns instance of ActiveSupport::Duration
(which allows pretty #inspect
and other goodies), but Ruby’s Time
doesn’t give a damn about ActiveSupport::Duration
. So, we just do
more patches! We replace Time#+
and Time#-
with our own versions,
which are ActiveSupport-aware (with fallback to “native” version, so
we still can do just Time.now + 30
).
So, once you start trying to be “natural” and intervene some core structures with your own cool additions, you eventually find yourself “fixing” the entire language here and there, bless Ruby for allowing it.
3. To fix something we consider broken
That is mostly related to third-party libraries, not Ruby core classes or stdlib itself. You need to get your things done, you have some class or module that behaves wrong (bug) or “wrong” (incovenient to you), you read the source, then redefine one or two methods in your own code, and you are good.
Until the next version of a patched library, of course.
4. To place behavior where it belongs
Like in a definition of object, you know? “Object = data + behavior”. Ruby’s almost pure object-orientedness, concise syntax and praising of method chains often make us to want even more “concise and readable” things, adding new and new methods to core classes (numbers, strings, collections).
Unlike #days
above, here we are talking about methods that actually
relevant to object’s context and responsibilities, not just an attempt
to make funny syntax.
Some examples:
-
Number#clamp(min, max)
—returns number which is definitely betweenmin
andmax
, e.g.-1.clamp(0, 10) #=> 0 100.clamp(0, 10) # => 10 5.clamp(0, 10) # => 5
When you have large formula or set of formulae, with several times said
return 0 if x < 0
, you’ll appreciate this a lot (BTW, already accepted as a Ruby standard feature); String#surround(before, after)
—while developing something heavily dependent on constructing strings from strings from strings, you’ll be happy with"foo".surround('(', ')') # => "(foo)"
;Array#sum
(nothing to explain here; BTW, also accepted in core for Ruby 2.4).
To be honest, that’s the best and, when used right, incredibly helpful
usage for open classes: when moving towards that best mix of OO and
functional we are all looking for currently, ability to do
the_value.something_reasonable
is a real gift from heavens. Sometimes.
Cool, then why monkey-patching is a questionable practice?
Patches are global, unstoppable, often unexpected and hard to detect.
You just require
someone else’s code (it even can be yours, you just
forgot about global effect of changes), and, next second:
- your code becames incredibly slow (because you rely on math, and
somebody redefined
Fixnum#+
to allow it being summed up with custom classes); - your duck typing breaks (because somebody loved to do “smart trick”
and add
String#to_a
, and all yourif arg.respond_to?(:to_a)
became false positives); - screwed up in any other way.
There are even more problems, typically associated with monkey patching, less severe yet still pretty ugly:
- patches conflict: if you are developing in conditions where every other library relies on monkey patching, earlier or later you’ll came to situation when there are two samely named methods from different sources with slightly or absolutely different behavior; one of libraries could be broken;
- scope pollution: eventually you have hundreds of methods defined on each and every object; that’s not that bad on its own, but leads to harder understanding and navigating through the code and again, to name conflicts.
Extensive monkey patching and ambivalence of our feelings about that is also a part of “Rails is a blessing/curse for Ruby” paradox.
Ruby is oblidged to Rails with its current popularity, but also with questionable reputation. Many of “Rails way” elements are associated with “how they do it in Ruby” and “too much magic”, including infamous “convention over configuration” principle—and extensive monkey-patching if whatever Rails team wants to “fix”.
At first, people love that you can just write that 1.day.ago
and that’s
look so expressive and readable… Then they became suspicious on reliability
and ubiquity of those little “convenience” tricks.
OK, then why not refinements?..
Addressing “open classes are cool”/”monkey patching is dangerous” dychotomy, Ruby 2 had introduced refinements.
That seemed (and still seems to some, including me) just “right” amount
of compromise: you want to make some complicated code simple and readable,
you refine your scope with any Integer#fancy_shmancy_method
you want,
but all other scopes stay unpolluted and everything seems under control.
Unfortunately, refinements happened to arrive half-baked, and since than, trapped into negative feedback loop: nobody loves them, nobody uses them, nobody cares about Ruby core team’s attempts to improve them; but it’s cool and fashionable to write your own “why refinements are bad” article or blog post. Most of those texts are true and useful (that’s the most recent one I saw), but I’m not sure that’s the only reaction refinements deserve.
In addition, JRuby doesn’t seem to support refinements decently, and, as far as I know, doesn’t even have plan to do that. So, if you are gem or other open source project author, relying on refinements becames almost as bad as relying on C extensions (or even worse, considering fact that popular C extensions, like Nokogiri, eventually get their JRuby version or analog… with refinements, you are just stuck).
So, maybe we should just abandon and forbid them totally?..
But, wait. Open classes of Ruby is a great playground for experimenting, finding the “right” ways and approaches. Our language is our tool, and, most important, its tool of thought, not just of constructing stuff you already know how to do. And openness of all and everything is good help on the way to what’s right and what will work.
RSpec, for one, is a great example of that (turn away for a moment, MiniTest fans! anyways, in the next post we’ll investigate that “MiniTest is just Ruby” rant).
It’s earlydays syntax
(2 + 2).should == 4
was definitely “too invasive” to be reliable, but its descendant
expect(2 + 2).to eq 4
is an excellent compromise: readable, flexible, extendable and way superior to
oldfashioned assert_that_really_important_thing
(you are still turned
away, MiniTest fans!)
Important thing is, RSpec’s brevity and readability changed the way of “specs” perception, and I believe it could not be achieved without “all is monkey-patched” step in progress. That’s how great Ruby tools emerge (and note that currently there is NO monkey patches RSpec requires), and that’s how the language itself evolves.
Of course, the reasoning above may seem irrelevant to you, especially if for you, your programming language is rather hammer than tool of thought.
When you are safe, and when you aren’t
OK, so let’s agree we will patch our monkeys until they would be really tired! Yet we still need some safety rules to not became monkeys ourself.
Let’s try to think of some:
-
First of all: think if your patch could be replaced with inheritance? If, for example, you want your arrays to have ton of statistical methods, (while still being a decent array), maybe it is reasonable to just do
class StatisticalArray < Array def mean sum / count end #.... end
-
Then: whether your patch could be replaced with module included on-the-fly?
module Stats def mean sum / count end end # This is not much better than just patch the class directly: Array.include Stats # ...though, provides _some_ useful information: [1,2,3].method(:mean) # => #<Method: Array(Stats)#mean> # ...but, you can just modify one instance of array to have necessary # functionality: [1,2,3].extend(Stats).mean # => 2 # Other array instance aren't polluted [4,5,6].mean # NoMethodError: undefined method `mean' for [4, 5, 6]:Array
- Than: selecting the name for your patch, try to use something
unambiguous (e.g.
Fixnum#positive?
… though, is0.positive?
); - …or something really specific to your project, like
String#to_our_company_logo
; - …or, at least, something rare and unlikely to clash with other’s ideas (see “My favorite patch” below);
- Adding new behavior (like
Time#monday?
) is safer than changing existing one (like rewritingTime#-
)—though the latter is often tempting, especially for infix math operators; - Using monkey-patching in home scripting and experiments is always safe,
though, consider it like testing approaches, not acquiring habits (e.g.,
don’t tug your entire favorite
core_ext.rb
in your next commercial or OSS project); - Using monkey-patching in gems you hope to be popular is almost always really bad decision, or at least requires serious consideration;
- Never (or at least try to never) use monkey patching in gems if it is only
for internal needs of the gem! For example, while developing daru
we eventually removed all
Array
extensions we needed for cleaner code inside, and left onlyArray#daru_vector
for converting native arrays to our types (which is also a good choice of specific yet short name);- Sometimes I break this rule too. See “My favorite patch” section for example and justification.
- Using monkey patches in applications is moderately safe; you still could have pretty ugly name clashes (especially if using ActiveSupport, which you probably do); you still should consider good architecture (e.g. do not attach behavior to unrelated data) and overall readability and predictability… Like, you know, always and with any language features;
- It is almost safe to use “backporting” monkey-patches, to use feature of newest versions of Ruby with older ones. There is excellent backports gem, but it is unfortunately stale on version 2.1.0;
- Finally, if with monkey patch you fix or enchance some third party gem (fixing its classes, not core ones), consider providing a PR to gem’s author instead, it is greatly appreciated by community!
On patches collections
Many projects have gathered tons of useful monkey-patches into gems. Most prominent are probably ActiveSupport of Rails and Ruby Facets.
I have a strong opinion (through my years with Ruby, I’ve developed a lot of them) that grouping patches in gem are almost never helpful (OK, unless ActiveSupport is a part of your framework of choice). Different “patches packs” have different attitudes and semantics for lot of their features, so, when you try to “have all the best (and nothing else)”, you probably end up with “just take ActiveSupport” (which is behemoth not suitable for all kinds of projects), and loose all other possibilities.
So, once I’ve even tried to do “the right thing”: community-driven
list of copy-pasteable snippets,
which you just pick for your own core_ext.rb
.
Still don’t know whether it is the right thing to do. And in fact, you
can use any other “patches pack” gem’s code as part of the “community
repository of patches” (I never depend on ActiveSupport, but frequently
steel from it).
My favorite patch
There is one little thing, that I can’t live without since I “invented” it (and immediately found out that there are hundreds of previous “inventors”), and even break some of “safety rules” described above.
It is infamous yield self
trick (one of first
proposals in Ruby tracker, latest discussion).
It looks like entire community agree that “we need it, just can’t name
it” (the latter link lists all the names people came up with so far…
like, 30 of them).
Why this is important and useful?
Imagine the code:
response = MyHTTPLibrary.get('http://api.example.com/list.json')
parsed = JSON.parse(response.body)
count = parsed['count'] || parsed['meta']['count']
count.to_i
It is almost inevitable, but has several typical “smells”, like short-living variables and being imperative more than functional.
Now, imagine that:
MyHTTPLibrary.get('http://api.example.com/list.json').body
.some_method(&JSON.method(:parse))
.some_method { |parsed| parsed['count'] || parsed['meta']['count'] }
.to_i
Isn’t it much better? (If your answer is “no”, you can skip the rest of the section, I will not be offended ;)).
For me, the code with some_method
is 10x more clean and readable. It
is like Elixir’s pipes,
but in most Rubyish way possible.
And, as a side note, having this in language core seriously saves from monkey patches! E.g. why have
.to_json
attached to all core classes, when you can just use the following code and call it a day?
['test', 'me'].some_method(&JSON.method(:generate))
The discussion for “better name” is lazily held since 2012 (can you imagine that?..), but in the meantime I started to use it in my code, even in gems.
What’s about the name?.. My current favorite for Ruby core is itself
(which we already have, it just should take a block as an argument), but
redefining one of such a basic things in a gem code is unforgivable sin.
So, I just stick to a name derp
: it is short, resembles tap
and
dumb enough to avoid name clashes in the most of code.
You can find this kind of code everywhere in my gems:
make_query(selectors)
.derp { |query| http_client.get(query, format: :json) }
.derp { |res| Wikidata::Entity.from_sparql(res.body) }
One-line conclusion
Monkey patches are useful, questionable and need to be mastered by any professional rubyist.
That’s, basically, it.
And use your head and taste constantly.