Wednesday, September 14, 2011

Once Bitten, Twice Shy


Got bitten but the Ruby Constants Are Not Constant bug again. Actually, it was in the same part of code that I'd found it before, only dealing with a portion of a hash a bit more deeply nested.

SOMEHASH = { 
 :elem1 => { 
   :props => [ :a, :b, :c ] 
 }, 
 :elem2 => { } 



hash = SOMEHASH.clone 
  # replaces SOMEHASH[:elem1][:props]

hash[:elem1][:props] = hash[:elem1][:props].shuffle  


I know, you Rubistas are going to say it isn't a bug; that it is just my naivete; that in Ruby, deep structures like nested arrays and hashes contain references to objects that do not change but the objects that are referenced can mutate.

What is happening here is that the anonymous hash (the object pointed to by the key :elem1) is not being copied. Its reference is what is copied. So shuffling and replacing one of its members (:prop1) necessarily changes SOMEHASH[:elem1].

One approach is to freeze the hash, but you'll end up forcing your app to abort when it tries to manipulate values. In our app, we were creating copies of elements and shuffling them. The problem being that our copies weren't really full copies: they contained references to the objects still in our original "Constant" hash.

And Matz might argue that it is not surprising, once you understand the details well enough. But it is still undesirable. I find many aspects of Ruby to be elegant, but not this one.

A more gruesome, brute-force approach is to deep copy the hash by serializing the data to a stream, and marshaling it back into a new hash object. It is a one-liner, but that one line is doing a lot of work in order to copy an hash.

def deep_copy(hash)
  Marshal::load(Marshal::dump(hash))
end
Post a Comment