Reputation: 12351
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
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.
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}]
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]
Upvotes: 1
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
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