views:

178

answers:

3

Let's say I want some instance of String behave differently from other, "normal" instances - for example cancel the effect of the "upcase" method. I do the following:

class String
  def foo
    def self.upcase
      self
    end
    self
  end
end

It seems to work fine, and the way I need it:

puts "bar".upcase #=> "BAR"
puts "bar".foo.upcase #=> "bar"

However, as soon as I use the tricked instance of the String as a key for a Hash, the behavior starts looking weird to me:

puts ({"bar".foo => "code"}).keys.first.upcase #=> "BAR", not "bar"!

... which is as if the foo method is ignored, and the original instance of String is used as the key.

Anyone can see what's going on here? Thanks a bunch!

A: 

The usual way to extend a single object in Ruby is this:

s = "bar"
class<<s
  def self.upcase
    self
  end
end

...but that doesn't solve your problem. It seems that Ruby has special rules for hash keys that are strings, or subclasses of strings Maybe instead of a string, you can use an object with a meaningful definition of to_s?

David Seiler
A: 

Just because in Ruby you can reopen core classes and virtually re-define everything, it doesn't mean you should.

With great powers come great responsibilities and your responsability is to not redefine a core library method just because a couple of objects might need this. If your instance doesn't behave like a Sting, declare your own class and extend String.

Simone Carletti
+5  A: 

Ruby's Hash has a special case for using strings as a hash key -- it makes an internal copy of the string.

Basically it's to protect you from using a string (object) as a key and then altering that string object later in the code, which could lead to some confusing situations. Mutable keys get tricky.

Rather than hack method onto string that returns an altered string class, I would just create a new subclass of string that overrides upcase and then just set its value.

Eli
I don't think making a subclass will work. The "def self.foo" is implemented with an anonymous subclass of the defining object's class, and that demonstrably doesn't work, so I think Ruby makes a copy of s if s.kind_of? String, not s.is_a? String.
David Seiler
So if I add a function to an instance of an object, it changes that object into an anonymous subclass of whatever it was before? I didn't realize that. If that's the case, it seems you're left with only ugly solutions here (aside from, well, don't use hacked versions of String as a key)
Eli
Nope, Eli's suggestion works : http://gist.github.com/138566 (feel free to copy this code, Eli). David, `def self.foo` affects the object's metaclass, and doesn't change what the class is.
rampion