Friday, September 4, 2009

The Primitive Obsession in Ruby aka Don't make EVERYTHING an Object

         When I was playing around with the sudoku solver I fell into several traps. One of these was a little insidious. Back in the day, I thought EVERYTHING had to be represented as an Object. (yes, I know everything in ruby IS an object, I just mean you don't have to create your own class for every concept). So one of the first objects I built was a position object. Every cell had a position and a value. So position was a little object with an x and a y value, representing it's coordinates. It could give me positions of neighbouring cells, so that was it's behaviour. 

         Now, one of the simplest methods of crafting the solver is to say, pick a cell, find all empty neighbours, and then figure out what to play in them. So I had some code of the form cell.position.neighbours.select(&:empty). But I had defined neighbours as row positions + column positions + block positions. So I was bound to end up with a lot of duplicate positions. This wasn't a problem per se, but it was bound to slow down the solver. So I decided to make it cell.position.neighbours.select(&:empty).uniq. But ofcourse, this didn't work. I had to make sure that 2 positions were actually equal if they had the same value. That was simple, we've all seen it in basic textbooks.



class Position
def ==(other)
other.equal?(self) || (other.instance_of?(self.class) && other.x == @x && other.y == @y)
end

def hash
@x*29 + @y
end
end

 
    But that didn't work. Turned out there is == for equality, but there is also eql?. Which is used in arrays and hashes while comparing objects. (The use case for eql? being different from == seems obscure, but it apparently ensures that 17 == 17.0 but 17 is not eql? 17.0).Anyway, that was easily fixed. I just had to override eql? and call ==



class Position
def eql?(other)
self == other
end
end

     Well it worked, but ... holy crap. Performance was abyssmal. Earlier Ive posted solver times as being in the order of several seconds. But initially the solver took several minutes. It took nearly 20 minutes with the constraints checking solver. I was quite shocked. So I tried various tricks including profiling the code, and it turned out MOST of the time was spent in uniq ... and withing that in my eql? definition. Redefining eql? killed the system. So I ended up modifying my objects so that I could depend on pure reference equality and suddenly everything was blazingly fast again. Later ofcourse, I got rid of position. But the lesson I took away was, don't override eql? and == in Ruby, unless you have a damn good reason.

No comments:

Post a Comment