rubymonkeypatchingruby-2.1ruby-2.2

Ruby refinement issues with respond_to? and scoping


I'm trying to add an instance method foo to Ruby's Array class so when it's invoked, the array's string elements are changed to string "foo".

This can be done easily by monkey patching Ruby's String and Array classes.

class String
  def foo
    replace "foo"
  end
end

class Array
  def foo
    self.each {|x| x.foo if x.respond_to? :foo }
  end
end

a = ['a', 1, 'b']
a.foo
puts a.join(", ")   # you get 'foo, 1, foo' as expected

Now I'm trying to rewrite the above using Ruby 2's refinements feature. I'm using Ruby version 2.2.2.

The following works (in a file, eg. ruby test.rb, but not in irb for some reason)

module M
  refine String do
    def foo
      replace "foo"
    end
  end
end

using M
s = ''
s.foo
puts s      # you get 'foo'

However, I can't get it to work when adding foo onto the Array class.

module M
  refine String do
    def foo
      replace "foo"
    end
  end
end

using M

module N
  refine Array do
    def foo
      self.each {|x| x.foo if x.respond_to? :foo }
    end
  end
end

using N

a = ['a', 1, 'b']
a.foo
puts a.join(", ")   # you get 'a, 1, b', not 'foo, 1, foo' as expected

There're two issues:

  1. After you refine a class with a new method, respond_to? does not work even when you can invoke the method on an object. Try adding puts 'yes' if s.respond_to? :foo as the last line in the second code snippet, you'll see 'yes' is not printed.
  2. In my Array refinement, the String#foo is out of scope. If you remove if x.respond_to? :foo from the Array#foo, you'll get the error undefined method 'foo' for "a":String (NoMethodError). So the question is: how do you make the String#foo refinement visible inside the Array#foo refinement?

How do I overcome these two issues so I can get this to work?

(Please don't offer alternative solutions that don't involve refinement, because this is a theoretical exercise so I can learn how to use refinement).

Thank you.


Solution

    1. The respond_to? method does not work and this is documented here.

    2. The problem is that you can only activate a refinement at top-level and they are lexical in scope.

    One solution would be:

    module N
      refine String do
        def foo
          replace 'foobar'
        end
      end
    
      refine Array do
        def foo
          self.each do |x|
            x.foo rescue x
          end
        end
      end
    end
    
    using N
    
    a = ['a', 1, 'b']
    p a.foo
    
    puts a.join(", ") # foo, 1, foo