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.

module BoolUniq
  refine Array do
    def uniq?
      !self.dup.uniq!
    end
  end
end

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.

class A
  def test
    [1,2,3].uniq?
  end
end

A.new.test
# NoMethodError: undefined method `uniq?' for [1, 2, 3]:Array

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:

class A
  def another_method
  end
end

A.instance_methods - Object.methods
# => [:test, :another_method]

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.

class A
  using BoolUniq
end

A.new.test
# NoMethodError: undefined method `uniq?' for [1, 2, 3]:Array

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.

class A
  using BoolUniq

  def test2
    [1,2,3].uniq?
  end
end

A.new.test2
# => true

A.new.test
# NoMethodError: undefined method `uniq?' for [1, 2, 3]:Array

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.

class A
  def test2
    [1,2,3].uniq?
  end
end

A.new.test
# NoMethodError: undefined method `uniq?' for [1, 2, 3]:Array

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?

#1
!self.dup.uniq!

#2
self.length == self.uniq.length

#3
self.sort == self.uniq.sort

#4
self.each_with_index { |a,b|
  self.each_with_index { |c,d|
    next if b == d
    return false if a == c
  }
}
true

# 5
self.combination(2).each {|a,b| return false if a == b}
true

# 6
self == self.uniq

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

#1        1.830000   0.010000   1.840000 (  1.830531)
#2        1.880000   0.000000   1.880000 (  1.882458)
#3        2.590000   0.000000   2.590000 (  2.596115)
#4       42.560000   0.070000  42.630000 ( 42.670076)
#5       26.250000   0.050000  26.300000 ( 26.331753)
#6        1.920000   0.000000   1.920000 (  1.927852)

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:

#1        2.020000   0.010000   2.030000 (  2.025461)
#2        2.000000   0.000000   2.000000 (  2.014741)
#3        2.040000   0.010000   2.050000 (  2.039809)
#4        2.030000   0.000000   2.030000 (  2.038861)
#5        2.000000   0.010000   2.010000 (  2.009482)
#6        1.980000   0.000000   1.980000 (  1.989207)

#1        2.080000   0.000000   2.080000 (  2.081751)
#2        2.080000   0.010000   2.090000 (  2.082846)
#3        2.100000   0.000000   2.100000 (  2.108430)
#4        2.090000   0.000000   2.090000 (  2.097069)
#5        2.090000   0.010000   2.100000 (  2.091239)
#6        2.080000   0.000000   2.080000 (  2.088127)

#1        2.180000   0.000000   2.180000 (  2.184355)
#2        2.180000   0.010000   2.190000 (  2.186166)
#3        2.190000   0.000000   2.190000 (  2.200226)
#4        2.230000   0.010000   2.240000 (  2.240560)
#5        2.180000   0.000000   2.180000 (  2.182407)
#6        2.200000   0.010000   2.210000 (  2.210603)

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

Comments

  1. Andrew Kozin
    June 21, 2015 - 5:12 am

    Hi, Daniel! Have you seen this talk http://www.youtube.com/watch?v=_27-4-dbnA8 by Paolo Perotta, explaining why refinements are lexically scoped? After this I became thinking the “strictness” of refinements is not the bug, but a feature. Or kind of featurish.

    • Daniel P. Clark
      June 21, 2015 - 1:29 pm

      Thanks! I had not seen it.

      I hadn’t meant to imply with my post that the scope was a problem… it’s by design. The second half of my post was really the point on why I don’t think it’s production ready.

      I like the scoping. Once I can test it with indirect method calls it will be clear whether or not there are bug. The documentation clearly says that this “will” change, as in, they will have indirect method calls available to refinements in the future.

    • Daniel P. Clark
      June 21, 2015 - 1:04 pm

      After watching the video he has a great point on it being a potential security issue. Example:


      require "some_gem"

      SomeGem.methods
      # => [:trust_me, :no_harm, :all_clear]

      SomeGem.respond_to? :destroy_system
      # => false

      SomeGem.destroy_system
      ***KAABOOOOOM!!!!!!!!!!!****
      Segfault System Not Found

  2. John
    April 13, 2016 - 2:04 pm

    The dynamic dispatch issue is even worse than you describe, because it means `array.map { |x| x.refinement }` works fine, but `array.map(&:refinement)` will fail to find your function.

    • Daniel P. Clark
      April 13, 2016 - 2:18 pm

      I’ve submitted my test results to the Ruby Bug issue tracker https://bugs.ruby-lang.org/issues/11704

      But as for your point… Wow! You’re right!

      
      module Moo
        refine Fixnum do
          def to_s
            "moo"
          end
        end
      end
      
      class A
        using Moo
        def a
          [1,2,3].map {|x| x.to_s }
        end
      
        def b
          [1,2,3].map(&:to_s)
        end
      end
      
      A.new.a
      # => ["moo", "moo", "moo"] 
      A.new.b
      # => ["1", "2", "3"]
      

      If you believe this is related directly to my issue please add this information to the issue in the bug tracker. If it’s different and not already in the issue tracker please add it.

Leave a Reply

Your email address will not be published / Required fields are marked *