Last week a colleague was telling me about some difficulty he was having convincing someone that a web-browser based expandable tree view of data couldn’t be all that difficult. You know the sort of thing, very similar to a graphical file system explorer where you can expand branches and generally have a good sniff around your hierarchical data set.
It nagged at me the rest of the day. How hard could it be to do? I finally cracked and decided to whip up a quick Rails prototype. 2 hours later and I was done. With test data in my database I could use my browser just like Windows Explorer, expanding branches merrily as I went. All was good. Or so I thought.
On closer inspection, something was wrong. The code that was called when the user wanted to collapse a branch wasn’t working right, any expanded sub-branches remained open. I was holding a list of the expanded branches in the user’s session, and all the code had to do was remove the collapsing branch (and any of its expanded children) from the list.
Here’s the original code (the model is using acts_as_nested_set, which is why the logic to check whether a node is a child of the one being collapsed is coded the way it is):
session[:expanded].each do |party|
if(collapsed.lft < party.lft && collapsed.rgt > party.rgt)
session[:expanded].delete party
end
end
Hmmm. It seems to be doing the right thing. Old Ruby hands (and those with more switched on brains than me when I wrote this) will already see what took me a long time to find out with breakpoints and irb. As I looked at the code execution at breakpoints I fell into an old, old trap. Using acts_as_nested_set was something new for me, whereas handling arrays was as old as the hills. I managed to convince myself that the problem was in the “if…” logic. There must be an error in it that’s failing to properly identify children of the node being collapsed. It took me a while to finally admit that, no, the logic was right and the error was elsewhere.
As soon as I did that, the answer hit me. I was deleting items from an array while I was iterating over it with each. This is the Ruby equivalent of crossing the streams. Every atom in your body won’t simultaneously explode, but something bad will definitely happen.
It didn’t take long to find an answer, thanks to my new friend delete_if. This handy method takes a block, and deletes all elements of an array for which the block returns true. The balance of the universe is restored, and I can feel happy at learning lots more about debugging Ruby with breakpoints and irb.
Remember, don’t cross the streams!