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)!

Advertisements
This entry was posted in Ruby.

4 comments on “Odd Rubyism

  1. José Valim says:

    Very odd inded!
    Have you checked Ruby 1.9 if we have the same behavior? =)

  2. Bill Kayser says:

    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.

  3. John Stehle says:

    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.

  4. James Robey says:

    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.

Comments are closed.