Learning Ruby, I'm creating a Battleship project and I have the following code as an instance method for a class "Board" I'm creating.
def hidden_ships_grid
hidden_s_grid = @grid.dup
hidden_s_grid.each_with_index do |sub_array, i|
sub_array.each_with_index do |el, j|
# position = [i, j]
hidden_s_grid[i][j] = :N if el == :S
end
end
end
Basically this method would create another instance of a @grid variable that would replace every :S symbol with a :N instead.
The RSPEC has two requirements: 1) "should return a 2D array representing the grid where every :S is replaced with an :N" and 2) "should not mutate the original @grid".
My problem is that my above code satisfies the first requirement, but it breaks the second requirement. Can someone please explain to me what is causing the original @grid file to be mutated? I've gone through the code 15 times over and I can't see where I rewrite or reassign the original @grid variable.
The "correct" solution provided to us uses ".map" which is fine, but I want to understand why this solution isn't working and ends up mutating the original @grid variable.
1) Board PART 2 #hidden_ships_grid should not mutate the original @grid
Failure/Error: expect(board.instance_variable_get(:@grid)).to eq([[:S, :N],[:X, :S]])
expected: [[:S, :N], [:X, :S]]
got: [[:N, :N], [:X, :N]]
(compared using ==)
Diff:
@@ -1,2 +1,2 @@
-[[:S, :N], [:X, :S]]
+[[:N, :N], [:X, :N]]
This is a common newbie mistake.
Suppose
a = [1, 2, 3]
b = a.dup
#=> [[1, 2], [3, 4]]
b[0] = 'cat'
#=> "cat"
b #=> ["cat", 2, 3]
a #=> [1, 2, 3]
This is exactly what you were expecting and hoping for. Now consider the following.
a = [[1, 2], [3, 4]]
b = a.dup
#=> [[1, 2], [3, 4]]
b[0] = 'cat'
b #=> ["cat", [3, 4]]
a #=> [[1, 2], [3, 4]]
Again, this is the desired result. One more:
a = [[1,2], [3,4]]
b = a.dup
#=> [[1,2], [3,4]]
b[0][0] = 'cat'
b #=> [["cat", 2], [3, 4]]
a #=> [["cat", 2], [3, 4]]
Aarrg! This is the problem that you experienced. To see what's happening here, let's look the id's of the various objects that make up a
and b
. Recall that every Ruby object has a unique Object#id.
a = [[1, 2], [3, 4]]
b = a.dup
a.map(&:object_id)
#=> [48959475855260, 48959475855240]
b.map(&:object_id)
#=> [48959475855260, 48959475855240]
b[0] = 'cat'
b #=> ["cat", [3, 4]]
a #=> [[1, 2], [3, 4]]
b.map(&:object_id)
#=> [48959476667580, 48959475855240]
Here we simply replace b[0]
, which initially was the object a[0]
with a different object ('cat'
) which of course has a different id. That does not affect a
. (In the following I will give just the last three digits of id's. If two are the same the entire id is the same.) Now consider the following.
a = [[1, 2], [3, 4]]
b = a.dup
a.map(&:object_id)
#=> [...620, ...600]
b.map(&:object_id)
#=> [...620, ...600]
b[0][0] = 'cat'
#=> "cat"
b #=> [["cat", 2], [3, 4]]
a #=> [["cat", 2], [3, 4]]
a.map(&:object_id)
#=> [...620, ...600]
b.map(&:object_id)
#=> [...620, ...600]
We see that the elements of a
and b
are the same objects as they were before executing b[0][0] = 'cat'
. That assignment, however, altered the value of the object whose id is ...620
, which explains why a
, as well as b
, was altered.
To avoid modifying a
we need to do the following.
a = [[1, 2], [3, 4]]
b = a.dup.map(&:dup) # same as a.dup.map { |arr| arr.dup }
#=> [[1, 2], [3, 4]]
a.map(&:object_id)
#=> [...180, ...120]
b.map(&:object_id)
#=> [...080, ...040]
Now the elements of b
are different objects than those of a
, so any changes to b
will not affect a
:
b[0][0] = 'cat'
#=> "cat"
b #=> [["cat", 2], [3, 4]]
a #=> [[1, 2], [3, 4]]
If we had
a = [[1, [2, 3]], [[4, 5], 6]]
we would need to dup
to three levels:
b = a.map { |arr0| arr0.dup.map { |arr1| arr1.dup } }
#=> [[1, [2, 3]], [[4, 5], 6]]
b[0][1][0] = 'cat'
b #=> [[1, ["cat", 3]], [[4, 5], 6]]
a #=> [[1, [2, 3]], [[4, 5], 6]]
and so on.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With