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:
- 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 addingputs 'yes' if s.respond_to? :foo
as the last line in the second code snippet, you'll see 'yes' is not printed. - 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 errorundefined 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.
The
respond_to?
method does not work and this is documented here.The problem is that you can only activate a refinement at top-level and they are lexical in scope.
One solution would be: