Christopher Byrd Christopher Byrd - 3 months ago 17
Ruby Question

Using self.dup, but failing rspec test to not modify original array

I'm creating a method to transpose square 2-d arrays. My method passes every test, except the "does not modify original array" one. I'm only working on the duped array, so I'm confused on why the test is failing.

Code:

class Array
def my_transpose
orig_arr = self.dup; array = []
orig_arr[0].length.times do
temp_arr = []
orig_arr.each { |arr| temp_arr << arr.shift }
array << temp_arr
end
array
end
end


RSpec:

describe Array do

describe "#my_transpose" do
let(:arr) { [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
] }

let(:small_arr) { [
[1, 2],
[3, 4]
] }

it "transposes a small matrix" do
expect(small_arr.my_transpose).to eq([
[1, 3],
[2, 4]
])
end

it "transposes a larger matrix" do
expect(arr.my_transpose).to eq([
[1, 4, 7],
[2, 5, 8],
[3, 6, 9]
])
end

it "should not modify the original array" do
small_arr.my_transpose

expect(small_arr).to eq([
[1, 2],
[3, 4]
])
end

it "should not call the built-in #transpose method" do
expect(arr).not_to receive(:transpose)

arr.my_transpose
end
end
end


Output:

7) Array#my_transpose should not modify the original array
Failure/Error: expect(small_arr).to eq([

expected: [[1, 2], [3, 4]]
got: [[], []]

(compared using ==)
# ./spec/00_array_extensions_spec.rb:123:in `block (3 levels) in <top (required)>'

Answer

When you call dup on an array, it only duplicates the array itself; the array's contents are not also duplicated. So, for example:

a = [[1,2],[3,4]]
b = a.dup
a.object_id == b.object_id        # => false
a[0].object_id == b[0].object_id  # => true

Thus, modifications to a itself are not reflected in b (and vice versa), but modifications in the elements of a are reflected in b, because those elements are the same objects.

That being the case, the problem crops up here:

orig_arr.each { |arr| temp_arr << arr.shift }

arr is an element of orig_arr, but it is also an element of self. If you did something like remove it from orig_arr, you would not also remove it from self, but if you change it, it's changed, no matter how you are accessing it, and as it turns out, Array#shift is a destructive operation.

Probably the smallest change you could make to your code to make it work as you expect would be to use each_with_index, and then use the index into arr, rather than calling arr.shift, so:

orig_arr.each_with_index { |arr,i| temp_arr << arr[i] }

In fact, though, once you're doing that, you're not doing any destructive operations at all and you don't need orig_arr, you can just use self.