4

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]]
Nakilon
  • 34,866
  • 14
  • 107
  • 142

3 Answers3

7

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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Thanks, I forgot Ruby doesn't have `deepcopy`, but I wonder why.. Why not add to 2.6? Python has it. – iGian Jan 24 '19 at 21:59
  • @iGan, good question. I'm sure the Ruby monks have considered it. One can get a deep copy for most objects with `Marshal.load(Marshall(dump(obj)))`. See [Marshal](https://www.quora.com/How-do-I-get-a-full-scholarship-to-the-top-US-universities). – Cary Swoveland Jan 24 '19 at 22:26
  • Rails has `deep_dup` and there are gems that offer this outside of Rails. Most all have abysmal performance. Which is logic if you understand what happens under the hood. A classic leaky abstraction, so I guess Ruby core will not implement it untill at least that issue is solved. – berkes Dec 12 '21 at 21:12
3

dup and clone are shallow You copy contents of the grid array that are references to inner array. Those arrays are not copied, but referenced. That's why original grid is chaged later.

You need to map over grid and dup inner arrays

mrzasa
  • 22,895
  • 11
  • 56
  • 94
-1

The dup and clone both are for make duplicates records.

You can refer this link for dup Dup Api Documents

You can refer this link for cloneclone Api Documents

Foram
  • 483
  • 5
  • 12