EdSF
EdSF

Reputation: 12351

Ruby map, map! modifying array of hashes

So map vs map!

foo = [1,2,3]
foo.map { |i| i*=2}
=> [2, 4, 6]
foo
=> [1, 2, 3] # foo unchanged

foo.map! { |i| i*=2}
=> [2, 4, 6]
foo
=> [2, 4, 6] # foo is changed

All good/expected. Now with Array of hash, let's do map:

bar =  [{foo:1,bar:11}, {foo:2,bar:12}, {foo:3,bar:13}]
=> [{:foo=>1, :bar=>11}, {:foo=>2, :bar=>12}, {:foo=>3, :bar=>13}]
bar.map { |i| i[:foo]*= 2}
=> [2, 4, 6]
bar
=> [{:foo=>2, :bar=>11}, {:foo=>4, :bar=>12}, {:foo=>6, :bar=>13}] # changed

So it seems that the underlying array of hashes were modified using map, and essentially is the same as map!:

bar.map! do |i| 
 i[:foo]*=2 
 i 
end
=> [{:foo=>2, :bar=>11}, {:foo=>4, :bar=>12}, {:foo=>6, :bar=>13}]
bar
=> [{:foo=>2, :bar=>11}, {:foo=>4, :bar=>12}, {:foo=>6, :bar=>13}]

Probably missing something fundamental here. Not looking for alternatives, just trying to understand what seems to be an undocumented(?) note/gotcha/inconsistency. Tnx!

Upvotes: 2

Views: 210

Answers (3)

iGian
iGian

Reputation: 11183

I'd like to add my two cents.

When you use the exact same code (e[:foo] *= 2) in the block you can see that map and map! differs.


Using map

bar =  [ {foo:1, bar:11}, {foo:2, bar:12}, {foo:3, bar:13} ]
bar.map { |e| e[:foo] *= 2 } #=> [2, 4, 6]
bar #=> [{:foo=>2, :bar=>11}, {:foo=>4, :bar=>12}, {:foo=>6, :bar=>13}]

On bar you get the exact same effect as using each, except for the returning value of the call:

bar =  [ {foo:1, bar:11}, {foo:2, bar:12}, {foo:3, bar:13} ]
bar.each { |e| e[:foo] *= 2 } #=> [{:foo=>2, :bar=>11}, {:foo=>4, :bar=>12}, {:foo=>6, :bar=>13}]
bar #=> [{:foo=>2, :bar=>11}, {:foo=>4, :bar=>12}, {:foo=>6, :bar=>13}]


Using map!

bar =  [ {foo:1, bar:11}, {foo:2, bar:12}, {foo:3, bar:13} ]
bar.map! { |e| e[:foo] *= 2 } #=> [2, 4, 6]
bar #=> [2, 4, 6]


Once @Amadam gave me a link to a really useful tool to understand what's going on during the execution of the code, maybe could help further: http://www.pythontutor.com/visualize.html#mode=edit

Upvotes: 1

pjs
pjs

Reputation: 19855

The map method does not change the array's contents, as can be seen by inspecting the object_id of each element of the array. However, each element is a hash, which is mutable, so updating the contents of the hash is permissible. This can be seen from the following step-by-step trace of the results:

p baz = [{foo:1,bar:11}, {foo:2,bar:12}, {foo:3,bar:13}]
puts "baz contents IDs are:"
baz.each { |hsh| p hsh.object_id }
puts "performing the map operation"
p baz.map { |i| i[:foo] *= 2 }
puts "baz contents IDs still are:"
baz.each { |hsh| p hsh.object_id }
puts "...but the contents of those contents have changed:"
p baz

which produces, e.g.:

[{:foo=>1, :bar=>11}, {:foo=>2, :bar=>12}, {:foo=>3, :bar=>13}]
baz contents IDs are:
70261047089900
70261047089860
70261047089840
performing the map operation:
[2, 4, 6]
baz contents IDs still are:
70261047089900
70261047089860
70261047089840
...but the contents of those contents have changed:
[{:foo=>2, :bar=>11}, {:foo=>4, :bar=>12}, {:foo=>6, :bar=>13}]

Note that I changed the name of the array to baz to avoid any confusion due to name shadowing.

Upvotes: 2

max pleaner
max pleaner

Reputation: 26758

Let's compare the value of i in your first example:

foo = [1,2,3]
foo.map { |i| i*=2}

i here is a number. Numbers are immutable. Thus writing i * 2 or i *= 2 in the block makes no difference. The assignment doesn't propogate anywhere, no matter if you use map or map!.

Why doesn't the assignment matter, even if you use map! ? We can re-implement map! quickly to understand:

def my_map!(list, &blk)
  list.each_index { |i| list[i] = blk.call(list[i]) }
  list
end

As you can see, we're setting the value of each index to the return value of the block. And the return value of i * 2 or i *= 2 is the same.

Now, looking at the second example:

bar =  [{foo:1,bar:11}, {foo:2,bar:12}, {foo:3,bar:13}]
bar.map { |i| i[:foo]*= 2}

Why does this mutate the hashes? The value of i here is a Hash, which is mutable. Assigning a key-val (i[:foo]*= 2) mutates it, regardless of if it happens in each, map, map!, etc.

So, to cut to the chase, you would want to create a new hash using something like merge or dup:

bar =  [{foo:1,bar:11}, {foo:2,bar:12}, {foo:3,bar:13}]
bar.map { |i| i.merge(foo: i[:foo] * 2) }

Upvotes: 2

Related Questions