Odd Rubyism

January 2, 2009 at 11:17 am 4 comments

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

Advertisement

Entry filed under: Ruby. Tags: .

Presenting gem_cloner Big changes in Rails coming

4 Comments Add your own

  • 1. José Valim  |  January 2, 2009 at 12:30 pm

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

  • 2. Bill Kayser  |  January 4, 2009 at 12:53 pm

    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  |  March 11, 2009 at 2:11 pm

    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  |  February 4, 2010 at 8:57 am

    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

Fill in your details below or click an icon to log in:

Gravatar
WordPress.com Logo

Please log in to WordPress.com to post a comment to your blog.

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Trackback this post  |  Subscribe to the comments via RSS Feed


Bio Pic

My picture

Other Info


Follow

Get every new post delivered to your Inbox.