Isaac Seessel
Isaac Seessel

Reputation: 1

Deep Copying of Objects Ruby

I finished the pecs of tic tac toe, and was working on improving my computer player. This necessitates creating copies of new board objects from old board objects. I am having trouble creating deep copies of a board.

Here is the code in question:

  Class Board
  attr_accessor :grid

  def initialize(grid = Array.new(3){ Array.new(3)})
    @grid = grid
  end

  def place_mark(cords, sym)
    self[cords] = sym
    @grid 
  end

  def [](pos)
    row, col = pos
    @grid[row][col]
  end

  def []=(pos, mark)
    row, col = pos
    @grid[row][col] = mark
  end

 def new_state
    grid = @grid.dup
    Board.new(grid)
  end
end 

board = Board.new
new_state = board.new_state # ==> A different object
new_state.place_mark([0,0], :X) # ==> Object with x placed at 0,0
board # ==> Object with x placed at 0,0

Now, when I implement a new_state and then place a mark on the new_state it also places a mark on the state that it was duplicated from.

I understand why if i set up my new_state with just duplicating the object it wouldn't work.But I don't understand why my current implementation doesn't work. I should be storing the grid of the current object, and then creating a new object with that same grid. Any thoughts?

Upvotes: 0

Views: 878

Answers (1)

Simple Lime
Simple Lime

Reputation: 11050

In Ruby, dup produces a shallow clone and doesn't copy the objects they reference. Since @grid is an array of arrays, each array in the @grid array is not copied. That may not have been clear, so hopefully code helps:

grid = [ [:first_row], [:second_row] ]
copy = grid.dup
grid.object_id == copy.object_id # => false, so you have a different array
grid[0].object_id == copy[0].object_id # => true, so the inner array is the same

copy[0][0] = :test_change
grid # => [[:test_change], [:second_row]]
copy # => [[:test_change], [:second_row]]

so the inner arrays are the same object, and modifying it from one place modifies everything referencing it. But, if you modify the 'outer' array, it works like you would expect:

copy[0] = [:updates_correctly]
copy # => [[:updates_correctly], [:second_row]]
grid # => [[:test_change], [:second_row]]

So, knowing all this, it's hopefully a little clearer how you would fix this, you just need to call dup on the 'inner' arrays. We'll use collect here as well, which returns a new array, so that we don't need to call dup on the 'outer' array at all:

copy = grid.collect(&:dup) # => [[:test_change], [:second_row]]
copy[0][0] = :another_change
copy # => [[:another_change], [:second_row]]
grid # => [[:test_change], [:second_row]]

but the elements of those arrays still aren't different objects, if we instead had strings:

grid = [ ['first row'] ]
copy = grid.collect(&:dup)

and modified the string in place, we'd end up changing both:

copy[0][0].upcase!
copy # => [["FIRST ROW"]]
grid # => [["FIRST ROW"]]

how deep you need to go depends on your particular needs.

So, TL;DR you need to change new_state to look like:

def new_state
  grid = @grid.collect(&:dup)
  Board.new(grid)
end

and it should work.

Upvotes: 1

Related Questions