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 10 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 10 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 10 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.