Bug #9776
closedRuby double-splat operator unexpectedly modifies hash
Description
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.
Updated by nobu (Nobuyoshi Nakada) over 11 years ago
- Backport changed from 2.0.0: UNKNOWN, 2.1: UNKNOWN to 2.0.0: DONTNEED, 2.1: REQUIRED
Updated by nobu (Nobuyoshi Nakada) over 11 years ago
- Status changed from Open to Closed
- % Done changed from 0 to 100
Applied in changeset r45724.
compile.c: non-destructive keyword splat
- compile.c (compile_array_): make copy a first hash not to modify
the argument itself. keyword splat should be non-destructive.
[ruby-core:62161] [Bug #9776]
Updated by nagachika (Tomoyuki Chikanaga) over 11 years ago
- Backport changed from 2.0.0: DONTNEED, 2.1: REQUIRED to 2.0.0: DONTNEED, 2.1: DONE
Backported into ruby_2_1 branch at r46451.