8

I am iterating over a nested array with two each blocks, and deleting an element from the same array inside the inner iteration:

arr = [1,2,3]  
arr.each do |x|  
  arr.each do |y|  
    puts "#{arr.delete(y)}"  
  end  
end

This produces the results 1, 3. The array becomes [2].

Why isn't the value 2 passed to the first or the second loop? Is this some sort of side effect of nested iteration?

3
  • please approve appropriate answer Commented Feb 19, 2016 at 8:05
  • @bronislav, give it some time. It's only been an hour. There's no rush. Commented Feb 19, 2016 at 8:46
  • @CarySwoveland, sorry Commented Feb 19, 2016 at 13:07

4 Answers 4

15

It's because of the index of the deleted element. I added some output to show you:

arr = [1,2,3]  
arr.each do |x|  
  puts "1: #{arr.inspect}, x: #{x}"
  arr.each do |y|  
    puts "2: #{arr.inspect}, y: #{y}"
    puts "#{arr.delete(y)}"  
  end  
end

Result:

1: [1, 2, 3], x: 1
2: [1, 2, 3], y: 1
1
2: [2, 3], y: 3
3
=> [2]

The first deleted element is 1 (index is 0) in the inner each block. After the deletion 2 has the index 0 and now the each iteration goes to index 1 which is now element 3. 3 will be deleted and that's the end of the iteration. So you get [2].

The same happens without nested each:

arr = [1,2,3]  
arr.each do |x|  
  puts "1: #{arr.inspect}, x: #{x}" 
  puts "#{arr.delete(x)}"  
end

Result:

1: [1, 2, 3], x: 1
1
1: [2, 3], x: 3
3
=> [2]

I suggest to use reverse_each for such operations to avoid this behavior:

arr = [1,2,3]  
arr.reverse_each do |x|  
  puts "1: #{arr.inspect}, x: #{x}" 
  puts "#{arr.delete(x)}"  
end

Result:

1: [1, 2, 3], x: 3
3
1: [1, 2], x: 2
2
1: [1], x: 1
1
=> []
Sign up to request clarification or add additional context in comments.

1 Comment

+1 of the constructive suggestion. Or you could just choose to use select or reject in these situations
3

It has nothing to do with nesting. In fact, you will get the same result with just the inner loop:

arr = [1,2,3]  
arr.each do |y|  
  puts "#{arr.delete(y)}"  
end  
# => outputs 1, 3
a # => [2]

The compication is due to modifying the array during iteration.


The reason is because Array#each is based on the index. First, x becomes 1 (which is entirely irrelevant to the result). Within the inner loop, first you have:

  • a: [1, 2, 3], index: 0, y: 1

where index is the index on which the inner iteration is based, and you delete y and you get:

  • a: [2, 3]

In the next inner iteration, you have:

  • a: [2, 3], index: 1, y: 3

Note that 2 is skipped because iteration is based on the index (1). Then, 3 is deleted, which gives:

  • a: [2].

When the outer loop tries the next iteration at index 1, there is not enough elements left in a, so it ends there.

Comments

1

To understand this case, let us take case of a simple array being traversed with index.

You have an array with [1,2,3].

When you start iteration with 0, current element is 1. Now, you delete the element 1 at index 0, your array will become [2,3].

In the next iteration, your index will be 1 and that will point to 3. And 3 will be deleted. Your array will be [2].

Now, your index is 2 and array has length 1. So, nothing will happen. Now, when this inner loop will complete, outer loop will resume at updated index 1 and then to 2. And as array has length of 1, they will not be executed.

So, going by this, it seems that is using index as iteration.

As per my knowledge, it should have undefined behaviour (like in C++ such code is not recommended). Because, while iterating if you delete the current element, it will corrupt the pointer value (currently being held in the parameter of function block passed to each).

1 Comment

Your first paragraph is your opinion/feature request, and should be kept separate from the rest (perhaps be put at the end within paraneheses). I think that is the reason you are not getting upvoted despite the concise answer. You also have no mention to the outer loop (At least you need to explain how it has no effect).
0

since the each iterates with the indexes and you are deleting an element in the inner loop the every other element in the next iteration. if you increase the number of elements and including the current index of the iteration in the loop you'll be able see the bigger picture.

arr = [1,2,3,4,5,6,7,8,9]  
arr.each_with_index do |x,ix|  
  puts "loop1: #{arr.inspect}, x: #{x}, ix: #{ix}"
  arr.each_with_index do |y, iy|  
    puts "loop2: #{arr.inspect}, y: #{y}, iy: #{iy}"
    puts "#{arr.delete(y)}"  
  end  
end

result

loop1: [1, 2, 3, 4, 5, 6, 7, 8, 9], x: 1, ix: 0
loop2: [1, 2, 3, 4, 5, 6, 7, 8, 9], y: 1, iy: 0
1
loop2: [2, 3, 4, 5, 6, 7, 8, 9], y: 3, iy: 1
3
loop2: [2, 4, 5, 6, 7, 8, 9], y: 5, iy: 2
5
loop2: [2, 4, 6, 7, 8, 9], y: 7, iy: 3
7
loop2: [2, 4, 6, 8, 9], y: 9, iy: 4
9
loop1: [2, 4, 6, 8], x: 4, ix: 1
loop2: [2, 4, 6, 8], y: 2, iy: 0
2
loop2: [4, 6, 8], y: 6, iy: 1
6
 => [4, 8] 

since you are deleting during the loop and after each iteration the index is incremented but the array is one element short, so, it deletes the next (and all the) matching element(s) available and at the end the loop compares and stops the loop when index >= length

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.