2

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!

3 Answers 3

2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you, "hash is mutable" was how I was making sense of it. IMHO, worth a note in documentation(?) of map, map!....
It might be worth suggesting a comment in the documentation.
2

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) }

3 Comments

Thank you - "The value of i here is a Hash, which is mutable" - this was how I was groking it.
Can you pls clarify "The assignment doesn't propogate anywhere, no matter if you use map or map!" - re: map! assigns the new value as expected (in first example Array<number>)
@EdSF I can try ... consider [1,2,3].each { |i| i = 5000 }. What's getting assigned to 5000 here? The variable i, which is redefined each block.
1

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

1 Comment

Thank you - yup, understand that. I'm more into what happens to the underlying/original array, with map - re: it does change contrary to what one might think depending on mutability.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.