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 between min and max, 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 your if 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).

Okay.jpg

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, is 0.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 rewriting Time#-)—though the latter is often tempting, especially for infix math operators;
    • FYI: I break this rule myself in reality, providing better #inspect for some of stdlib classes… but it is optional and harmless (and proposed to be fixed in Ruby itself);
  • 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 only Array#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) }

DERP

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.