Odd Rubyism

Today I found a little counterintuitive oddity in Ruby.  Take the following code example:

a = [0,1,2,3,4,5,6,7,8,9,10]

h = {0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5,
     6=>6, 7=>7, 8=>8, 9=>9, 10=>10}

puts "Iterating & deleting array"
a.each do |i|

  puts "On #{i}"
  if i % 3 == 0

    puts "Deleting #{i}"
    a.delete(i)
  end

end

puts "Iterating & deleting hash"
h.each do |k,v|

  puts "On #{v}"
  if v % 3 == 0

    puts "Deleting #{v}"
    h.delete(k)
  end

end

With the output:

Iterating & deleting array
On 0
Deleting 0
On 2
On 3
Deleting 3
On 5
On 6
Deleting 6
On 8
On 9
Deleting 9
Iterating & deleting hash
On 5
On 0
Deleting 0
On 6
Deleting 6
On 1
On 7
On 2
On 8
On 3
Deleting 3
On 9
Deleting 9
On 4
On 10

Notice how each time the array deletes an item, it happens to skip and never evaluate the next item? Ruby internally uses an index counter (like if you were manually iterating the array with a for loop). When an item is deleted everything shifts back one.

With the Hash (it’s a little hard to see because they’re out of order) no items get skipped.

I’m not saying this is a bug or is necessarily wrong, but a Hash and an Array are very similar data structures (especially insofar as they are both enumerable) and I’m surprised that they work differently. I also spent a LONG time and a LOT of debugging to figure out that this was the error (it was buried deep in some XHR Rails calls, so reproducing it took a lot of time)

I’d love to hear from anyone who can attest to what happens in other language iterators (Java, Python, etc)!

4 thoughts on “Odd Rubyism”

  1. Java iterator behavior is well-defined and predictable. There are different iterator types that support different features. Generally if you delete or add an underlying element while iterating the iterator will throw a concurrent modification exception.

    Other iterator types support things like going forward and backward, inserting at the current index, and deleting the current item–without throwing the exception. That allows you to avoid the copy on read pattern to avoid the exception.

  2. Yes! that is not what one would expect. I ran across this behavior and did not know if it was as designed or not. What I do is nil the elements I want to delete then do a compact! on the array.

  3. Well, an item is being deleted from the array while the array itself is being used for the loop.

    Basically, it’s behaving normally, the array is shrinking, while the internal count is staying the same.

    [0,1,2], count is 0 item = 0

    delete 0, then increment count at end of loop

    [1,2], count 1 item = 2

    Hashes are unordered, so perhaps it doesn’t use an internal counter at all, but a pointer instead. Maybe that’s why the behavior is different.

Leave a Reply

Your email address will not be published. Required fields are marked *