Double-Splat Operator Destructively Modifies Hash - Is This a Ruby Bug

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

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.

What is the point of using Ruby's double-splat (`**`) in method calls?

The example using a single argument is the degenerate case.

Looking at a nontrivial case, you can quickly see the advantage of having the new ** operator:

def foo (args)
return args
end

h1 = { b: 2 }
h2 = { c: 3 }

foo(a: 1, **h1) # => {:a=>1, :b=>2}
foo(a: 1, **h1, **h2) # => {:a=>1, :b=>2, :c=>3}
foo(a: 1, h1) # Syntax Error: syntax error, unexpected ')', expecting =>
foo(h1, h2) # ArgumentError: wrong number of arguments (2 for 1)

Using the ** operator allows us to merge existing hashes together in the command line, along with literal key-value arguments. (The same is true of using * with argument arrays, of course.)

Sadly, you have to be careful with this behavior, depending on what version of Ruby you're using. In Ruby 2.1.1, at least, there was a bug where the splatted hash would be destructively modified, though it's since been patched.

Make Ruby object respond to double splat operator **

You can implement to_hash: (or define it as an alias for to_h)

class MyClass
def to_hash
{ a: 1, b: 2 }
end
end

def foo(**kwargs)
p kwargs: kwargs
end

foo(MyClass.new)
#=> {:kwargs=>{:a=>1, :b=>2}}

Unexpected result with splat operator

Because the []= method applied to hash takes only one argument in addition to the key (which is put inside the [] part), and a splatted/expanded array, which is in general a sequence of values (which coincidentally happens to be a single element in this particular case) cannot be directly accepted as the argument as is splatted. So it is accepted by the argument of []= as an array after all.

In other words, an argument (of the []= method) must be an object, but splatted elements (such as :foo, :bar, :baz) are not an object. The only way to interpret them as an object is to put them back into an array (such as [:foo, :bar, :baz]).

Using the splat operator, you can do it like this:

hash.each_pair{|key, value| hash.[]= key, *value}

What does a double * (splat) operator do

Ruby 2.0 introduced keyword arguments, and ** acts like *, but for keyword arguments. It returns a Hash with key / value pairs.

For this code:

def foo(a, *b, **c)
[a, b, c]
end

Here's a demo:

> foo 10
=> [10, [], {}]
> foo 10, 20, 30
=> [10, [20, 30], {}]
> foo 10, 20, 30, d: 40, e: 50
=> [10, [20, 30], {:d=>40, :e=>50}]
> foo 10, d: 40, e: 50
=> [10, [], {:d=>40, :e=>50}]

Expanding empty hash in variable with double splat in Ruby

Yes, it very looks like a bug

def foo(*args)
args
end

foo(**{})
# => []

h = {}

foo(**h)
# => [{}]

It passes empty hash as first argument in case of double splat of variable.

My version is ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin16]

Using splat operator with when

But the splat operator is about assignment, not comparison.

In this case, * converts an array into an argument list:

when *[2, 3, 4]

is equivalent to:

when 2, 3, 4

Just like in a method call:

foo(*[2, 3, 4])

is equivalent to:

foo(2, 3, 4)


Related Topics



Leave a reply



Submit