Double-splat operator destructively modifies hash – is this a Ruby bug?
Asked Answered
D

2

10

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 single-* 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.


Do we suppose the sometimes-destructive behavior of ** is intentional, or does it look more like a bug?

In either case, where is the documentation describing how the ** operator is meant to work?


I also asked this question in the Ruby Forum.

UPDATE

The bug is fixed in Ruby 2.1.3+.

Debug answered 25/4, 2014 at 0:49 Comment(9)
The use in parameter lists is in the core documentation ruby-doc.org/core-2.1.1/doc/syntax/methods_rdoc.html . Hash and array literal interpolation don't seem to appear anywhere there, though single spat at least has a spec: github.com/rubyspec/rubyspec/blob/master/language/splat_spec.rb. There's nothing similar for double splat. Ruby semantics do seem to be folkloric. I'm sure this is a bug insofar as an undocumented language feature can be buggy!Suckow
i did not even know that you are allowed to use that in a none method signature...Synthesis
It looks like the composed hash is the same object as the first element in it if it is a hash (they have the same object id). That is why they are modified. When you have two hashes h and i and do {**h, **i, d: 5}, only h is modified, not i.Charlinecharlock
One more thing - If you post directly on Rubyforum, it wouldn't be available in the mailing list, whereas reverse is ok. So better post it in the mailing list. What I said is the current Gateway problem.Hypocrite
@ArupRakshit I don't think it matters much. Ruby development community is slow in responding.Charlinecharlock
@Charlinecharlock I once had seen Are we dying?.. So asked him. :)Hypocrite
Interesting discovery. I find it a bug as the new introduced double splat operator introduced in Ruby 2.0 is an equivalent to existing single splat but for Hash instances and has only expand them unless used as a l-value but that's not this case.Orrery
@Charlinecharlock It's an interesting insight that the expression's result is the same object as h, but there's also more to it. Consider h = { a: 1 }; { **h, a: 99, **h }. Since the final result is { a: 99 }, we can see that even by the time we reach the final **h, h[:a] has already been overwritten.Debug
@JesseSielaff That is interesting. I feel it is a bug. I recommend you to post this as a bug on Ruby Trunk. Whether or not it is a bug, you will likely get response there.Charlinecharlock
D
7

The answers to the question seem to be:

  1. It's probably a bug, rather than intentional.

  2. The behavior of the ** operator is documented very briefly in the core library rdoc.

Thanks to the suggestions of several commenters, I've posted the bug to the Ruby trunk issue tracker.


UPDATE:

The bug was fixed in changeset r45724. The comment there was "keyword splat should be non-destructive," which makes this an authoritative answer.

Debug answered 25/4, 2014 at 20:22 Comment(2)
The bug already seems to have been fixed. That was quick.Charlinecharlock
@Charlinecharlock It's fast but need to say it looks like a hack then a fix. Just overwriting pointers from previous call with a shallow copy of Hash, omitting full condition. Feeling a bit embarrassed.Orrery
T
1

I noticed the diff between 2.1.5 and 2.3.1

Example is an irb method and a way of calling it

$ irb
>> def foo(opts) opts end
=> :foo
>> foo a: 'a', ** {a: 'b'}

In 2.1.5 the following results in retaining value

=> {:a=>"a"}

In 2.3.1 the value is 'b'

(irb):2: warning: duplicated key at line 2 ignored: :a
=> {:a=>"b"}

I am not sure which it should be?

In 2.3.1 the hash provided as double splat overrides the same key of the first item in a list.

Tibbs answered 27/7, 2016 at 18:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.