June 21, 2015 by Daniel P. Clark

Ruby Refinements – Not quite ready for production

I recently looked for a method on Array for a boolean response on its uniqueness.  As Ruby currently doesn’t have one I figured it’s easy enough to write one.  Now Ruby’s refinements are fairly new and a nice feature to have around.  So I decided to write a benchmark for different ways to test the uniqueness of an Array.  And I chose to do so with refinements.  In the process I ran into oddities and unexpected behavior I’d like to share with you – “the limitations Ruby refinements have as of Ruby 2.2.2”

If you’d like a basic overview on Ruby refinements see my blog post here: Refinements over Monkey-patching

Extremely Strict Scoping

This is by design, but the strictness of its design is quite startling.  First let us write our own refinement on Array for uniqueness checking.

Now we can use this within any class we write with using BoolUniq .  Let’s now write a class that will be using it.  For demonstration purposes we’ll leave out the refinement to see an expected exception.

This is as expected.  Now you may know that generally you can open up an existing class and add stuff to it such as follows:

With this thinking if you add refinements to a class with existing methods you “might” expect the behavior to happen to the entire new instance where the refinement is applied.  But this is not so.

So even though we told class A to include the refinement; existing methods remained untouched by the refinement.  So it turns out we need to define all the new methods/objects at the same time that we call using with our refinement for the refinement to take effect.  Let’s write the same exact method with a different name and use the refinement.

So even though we injected behavior within the same scope it did not touch anything previously defined.  Also if you re-open a class that has previously used a refinement and edit a method without explicitly calling the refinement again, you lose the effect of the refinement.

Invisibility

One thing I ran into is that the respond_to? method will always return false when asking a refined object if it responds to the method call.  This is true even if you evaluate a block of code within the scope/singleton-instance of a refined Object.  This is a known feature/bug and will be changed in the future (as noted in the official Refinement Specs).  So as of now there is no way to pry, peak, or reveal a refined method existence/source (which makes testing problematic).  You can only reap the result of its existence.

Indirect Method Calls – When using indirect method access such as Kernel#send, Kernel#method or Kernel#respond_to? refinements are not honored for the caller context during method lookup.

ruby-doc.org

It’s very much like voodoo code doing it’s magic and you can’t verify it is what you think it is.  And it gets worse.

Lost Refinements

During my benchmark tests I wanted to test different quantities of results.  (Here is a gist of the original benchmark I used testing 6 different ways to discover an Array’s uniqueness: https://gist.github.com/danielpclark/d27e3db346428a117712 )  I created a loop to go over different quantities but I found something terribly wrong with the results.  The first time the loop called each refinement to test I got differing results as one would expect.  But once the loop of refinements came up to be tested again everything washed into the same kind of results.

For example, testing these 6 methods on Array as the method :uniq?

The first time with an Array of 200 hexadecimal items gave these results: (smaller numbers are better)

Which has easily discernible differences between each test.  But on a continued loop testing the same refinements at quantities of 210, 220, and 230 all resulted into a wash of same results from which we can tell isn’t testing different code, but only one of the refinements:

Now we know something is wrong but we have no way to test what, or where, the source code is since refinements are invisible to external inspection.  I went ahead and changed my source code to not include the refinements within the benchmark test but to include  refined objects instead and got these same results.  So in my testing a loop of refinements for benchmarking the refinements got lost.  This should be a red flag for people considering them in production.  You may want to consider alternative solutions for now until refinements are ironed out.

Conclusion

Refinements are great!  Just be very cautious about using them if they may end up in scenarios like I’ve mentioned above.  I’m looking forward to when all the kinks are ironed out, but for now stick with tried and true ways when writing code for production!

Please feel free to comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!
-Daniel P. Clark

Image by Pete Zarria via the Creative Commons Attribution-NonCommercial-NoDerivs 2.0 Generic License

#change#code#exmaple#include#module#not ready#refinement#refinements#ruby