Yes, the with_index is a bit magic here.
Let me try to show by example what is actually going on:
First lets look at map. map iterates through the array and returns the result in the same place. So if we do elem + 1 we should end up with an array where each element have been increased by one.
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
arr.map { |elem| elem + 1 } # => [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
We can now look at select, which as the name suggests selects the elements and keep those we need. That is, if the block evaluate to true we keep the element else we discard it.
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
arr.select { |elem| elem == 5 } # => [5]
arr.select { |elem| elem > 7 } # => [8, 9, 10]
reject is just the opposite of select. Here we reject, if we evaluate to true
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
arr.reject { |elem| elem == 5 } # => [1, 2, 3, 4, 6, 7, 8, 9, 10]
arr.reject { |elem| elem > 7 } # => [1, 2, 3, 4, 5, 6, 7]
It is important to note that all of these methods return a new array and thus do not change the original.
with_index works on an enumerator, which map, select and reject (among others) return. So if we call with_index on the reject directly, without giving a block to reject we get the magic with_index construct. This will call the original enumerator (e.g. select or reject) but will yield both the element and the current index.
You can thus think of the with_index as changing your array into an array of pairs:
arr =[(1,0), (2,1), (3,2), (4,3), (5,4), (6,5), (7,6), (8,7), (9,8), (10,9)]
and if we call reject on these, we get a pair out, thus the two arguments. Since we only care about the index, and not the actual value of the element, we can ignore the first argument, which as a codestyle you show with _.