Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to delete from an Array inside each?

Tags:

arrays

ruby

Is it possible to safely delete elements from an Array while iterating over it via each? A first test looks promising:

a = (1..4).to_a
a.each { |i| a.delete(i) if i == 2 }
# => [1, 3, 4] 

However, I could not find hard facts on:

  • Whether it is safe (by design)
  • Since which Ruby version it is safe

At some points in the past, it seems that it was not possible to do:

It's not working because Ruby exits the .each loop when attempting to delete something.

The documentation does not state anything about deletability during iteration.

I am not looking for reject or delete_if. I want to do things with the elements of an array, and sometimes also remove an element from the array (after I've done other things with said element).

Update 1: I was not very clear on my definition of "safe", what I meant was:

  • do not raise any exceptions
  • do not skip any element in the Array
like image 598
NobodysNightmare Avatar asked Jul 08 '15 13:07

NobodysNightmare


2 Answers

You should not rely on unauthorized answers too much. The answer you cited is wrong, as is pointed out by Kevin's comment to it.

It is safe (from the beginning of Ruby) to delete elements from an Array while each in the sense that Ruby will not raise an error for doing that, and will give a decisive (i.e., not random) result.

However, you need to be careful because when you delete an element, the elements following it will be shifted, hence the element that was supposed to be iterated next would be moved to the position of the deleted element, which has been iterated over already, and will be skipped.

like image 97
sawa Avatar answered Oct 18 '22 17:10

sawa


In order to answer your question, whether it is "safe" to do so, you will first have to define what you mean by "safe". Do you mean

  1. it doesn't crash the runtime?
  2. it doesn't raise an Exception?
  3. it does raise an Exception?
  4. it behaves deterministically?
  5. it does what you expect it to do? (What do you expect it to do?)

Unfortunately, the Ruby Language Specification is not exactly helpful:

15.2.12.5.10 Array#each


each(&block)


Visibility: public

Behavior:

  • If block is given:
    1. For each element of the receiver in the indexing order, call block with the element as the only argument.
    2. Return the receiver.

This seems to imply that it is indeed completely safe in the sense of 1., 2., 4., and 5. above.

The documentation says:

each { |item| block }ary

Calls the given block once for each element in self, passing that element as a parameter.

Again, this seems to imply the same thing as the spec.

Unfortunately, none of the currently existing Ruby implementations interpret the spec in this way.

What actually happens in MRI and YARV is the following: the mutation to the array, including any shifting of the elements and/or indices becomes visible immediately, including to the internal implementation of the iterator code which is based on array indices. So, if you delete an element at or before the position you are currently iterating, you will skip the next element, whereas if you delete an element after the position you are currently iterating, you will skip that element. For each_with_index, you will also observe that all elements after the deleted element have their indices shifted (or rather the other way around: the indices stay put, but the elements are shifted).

So, this behavior is "safe" in the sense of 1., 2., and 4.

The other Ruby implementations mostly copy this (undocumented) behavior, but being undocumented, you cannot rely on it, and in fact, I believe at least one did experiment briefly with raising some sort of ConcurrentModificationException instead.

like image 29
Jörg W Mittag Avatar answered Oct 18 '22 17:10

Jörg W Mittag