Issue #9776 has been updated by Jesse Sielaff.


Origin:

http://stackoverflow.com/questions/23282342/ruby-double-splat-operator-destructive-vs-nondestructive

----------------------------------------
Bug #9776: Ruby double-splat operator unexpectedly modifies hash
https://bugs.ruby-lang.org/issues/9776#change-46318

* Author: Jesse Sielaff
* Status: Open
* Priority: Normal
* Assignee: Yukihiro Matsumoto
* Category: syntax
* Target version: current: 2.2.0
* ruby -v: ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-linux]
* Backport: 2.0.0: UNKNOWN, 2.1: UNKNOWN
----------------------------------------
I noticed what I find to be a very surprising behavior with the double-splat (`**`) operator in Ruby 2.1.1.

When key-value pairs are used before a `**hash`, the hash remains unmodified. However, when key-value pairs are only used after the `**hash`, the hash is permanently modified.

~~~
h = { b: 2 }

{ a: 1, **h }        # => { a: 1, b: 2 }
h                    # => { b: 2 }

{ a: 1, **h, c: 3 }  # => { a: 1, b: 2, c: 3 }
h                    # => { b: 2 }

{ **h, c: 3 }        # => { b: 2, c: 3 }
h                    # => { b: 2, c: 3 }

~~~

For comparison, consider the behavior of the splat (`*`) operator on arrays:

~~~
a = [2]

[1, *a]     # => [1, 2]
a           # => [2]

[1, *a, 3]  # => [1, 2, 3]
a           # => [2]

[*a, 3]     # => [2, 3]
a           # => [2]
~~~

The array remains unchanged throughout.

---------------

Tsuyoshi Sawada has also highlighted that the expression's result is the self-same object as the original hash:

~~~
h.object_id == { **h, c: 3 }.object_id # => true
~~~

---------------

I investigated `parse.y` to try to determine the error there, but I couldn't narrow it down any further than the `list_concat` or `rb_ary_push` function calls in the `assocs  :` block of the grammar.

Without exhaustively examining the C source, I think the best clue to the mechanism behind the erroneous behavior might be the following:

~~~
h = { a: 1 }
{ **h, a: 99, **h } # => {:a=>99}
~~~

That we don't see `{:a=>1}` illustrates that `h[:a]` is already overwritten by the time the second `**h` is evaluated.

---------------

Here is the use case that led me to this discovery:

~~~
def foo (arg) arg end

h = { a: 1 }

foo(**h, b: 2)

h # => { a: 1, b: 2 }
~~~

In the above example, I don't want `{ b: 2 }` permanently added to my existing hash. I'm currently solving it like this:

~~~
h = { a: 1 }

foo(**h.dup, b: 2)

h # => { a: 1 }
~~~

The call to #dup feels unnecessary, and is inconsistent with the analogous behavior when using the single `*` operator. If this bug is fixed, I'll be able to eliminate that call.




-- 
https://bugs.ruby-lang.org/