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

user513951
  • 12,445
  • 7
  • 65
  • 82
  • 2
    The use in parameter lists is in the core documentation http://www.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: https://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! – Gene Apr 25 '14 at 01:48
  • i did not even know that you are allowed to use that in a none method signature... – phoet Apr 25 '14 at 03:24
  • 2
    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`. – sawa Apr 25 '14 at 04:57
  • 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. – Arup Rakshit Apr 25 '14 at 05:32
  • @ArupRakshit I don't think it matters much. Ruby development community is slow in responding. – sawa Apr 25 '14 at 05:50
  • @sawa I once had seen [Are we dying?](https://www.ruby-forum.com/topic/4486783#new).. So asked him. :) – Arup Rakshit Apr 25 '14 at 05:53
  • 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. – David Unric Apr 25 '14 at 10:11
  • 3
    @sawa 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. – user513951 Apr 25 '14 at 18:26
  • @JesseSielaff That is interesting. I feel it is a bug. I recommend you to post this as a bug on [Ruby Trunk](https://bugs.ruby-lang.org/projects/ruby-trunk/issues). Whether or not it is a bug, you will likely get response there. – sawa Apr 25 '14 at 18:43

2 Answers2

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.

user513951
  • 12,445
  • 7
  • 65
  • 82
  • The bug already seems to have been [fixed](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/45724). That was quick. – sawa Apr 26 '14 at 13:37
  • @sawa 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. – David Unric Apr 26 '14 at 14:57
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.

marekj
  • 1,235
  • 2
  • 10
  • 11