Jin
Jin

Reputation: 1

Why does map function mutate array of objects when operating on each element's attribute?

I have an array of objects:

class Person
  attr_accessor :email

  def initialize(email)
    @email = email
  end
end

array = [
  Person.new('[email protected]'),
  Person.new('[email protected]')
]

I created a clone from the original array to perform map function, and then I mapped over each element to make its email attribute become uppercase:

clone = array.clone
clone.map { |obj|
  obj.email.upcase!
  obj
}

puts array.inspect # why is the original being mutated
puts clone.inspect

It mutates the original array. I have experimented with both dup and clone. and I get the same result. Why does map mutate the objects when operating on each element's attribute?

Upvotes: 0

Views: 312

Answers (1)

Amadan
Amadan

Reputation: 198436

You cloned the array containing Person references, but you did not change the array; you changed the Person instances themselves. clone is so-called "shallow clone", which copies only the receiver object, but none of the objects whose references it may contain.

In real-world logic: you took a piece of paper on which you wrote "Jenny, Timmy". Then you copied it to another piece of paper. You then took the first piece of paper, found the people it refered to, and gave them an apple. Then you took the second piece of paper, found the people on it, and wondered where their apples came from. But there's only one Timmy, only one Jenny: you give the first list's Jenny an apple, the second list's Jenny also has one.

If you want to clone something, clone Jenny.

array.map { |person|
  person.clone.yield_self { |clone|
    clone.email = clone.email.upcase
  }
}

(Note that I didn't use clone.email.upcase!. The reason is the same reason all over again: if you clone an object, they will both use the same string for email. upcase! changes that string, which would uppercase both clone's email and the original's email. Thus, we make a new email string for the clone.)

Things like this are best understood by stepping through the visualisation using this tool. However, the tool runs Ruby 2.2, which doesn't know about yield_self; this code is equivalent:

array.map { |person|
  clone = person.clone
  clone.email = clone.email.upcase
  clone
}

You could also write this, though it won't visualise as clearly:

array.map(&:clone).map { |clone|
  clone.email = clone.email.upcase
}

Upvotes: 3

Related Questions