Project

General

Profile

Actions

Bug #9776

closed

Ruby double-splat operator unexpectedly modifies hash

Added by jessesielaff (Jesse Sielaff) over 10 years ago. Updated over 10 years ago.

Status:
Closed
Target version:
ruby -v:
ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-linux]
[ruby-core:62161]

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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0