Bug #8507

Keyword splat does not convert arg to Hash

Added by Stephen Celis 10 months ago. Updated 3 months ago.

[ruby-core:55391]
Status:Feedback
Priority:Normal
Assignee:Yukihiro Matsumoto
Category:syntax
Target version:current: 2.2.0
ruby -v:ruby 2.0.0p195 (2013-05-14 revision 40734) [x86_64-darwin12.3.0] Backport:1.9.3: UNKNOWN, 2.0.0: UNKNOWN

Description

=begin
A single splat is useful to convert an object to an array as it passes from one method to the next, so I expected the double-splat to do the same and convert the object to a hash.

def splat args
p args
end
def double_splat *
kwargs
p args
end
splat(nil) # []
splat(
*nil) # TypeError: no implicit conversion of nil into Hash

For the sake of consistency, wouldn't it make sense to wrap the double-splatted arg with (({Hash()})) the same way a single-splatted arg is wrapped with (({Array()}))?
=end

to-hash-kwarg.patch Magnifier (939 Bytes) Stephen Celis, 06/10/2013 03:27 AM

History

#1 Updated by Yukihiro Matsumoto 10 months ago

  • Status changed from Open to Feedback

I hate consistency as a reason to change. Do you really want to do splat(**nil)?

Matz.

#2 Updated by Stephen Celis 10 months ago

Is there logic behind the current state that I'm unaware of? Consistency is predictability, which is important when learning a new interface. If splatted nils are swallowed, why not double-splatted nils? One raising where the other doesn't surprised me.

With args, you can be sure that the value passed is an array when splatted out. Splatting itself is seen as a method of conversion for me and other Rubyists I've spoken with. The fact that double-splatting makes no attempt to convert the value seems to make it useless as a convention. What is the difference between the following method invocations?

def kwmethod **kwargs
p kwargs
end

hash = { hello: 'world' }

kwmethod(hash)
kwmethod(**hash)

With *args, there is a clear reason to splat vs. not. Why not create a similar analog with keyword arguments?

#3 Updated by Stephen Celis 10 months ago

To return to the original question:

Do you really want to do splat(**nil)?

I find that Ruby's flexibility has been nice when providing public interfaces in gems and libraries. It's nice to be able to pass in nil to a public method and have it discard the argument as unnecessary. Likewise, it's nice to be able to accept *nil as the keyword argument to a public interface and have it discard the argument and look to its default keyword arguments instead.

#4 Updated by Yukihiro Matsumoto 10 months ago

You didn't explain why "it is nice". Could elaborate? Do we really need it?

FYI, in the early stage of Ruby, types are more flexible; integers can be treated as strings, nil can be treated as empty array. nil.to_a => [] is a left-over of the old days.

For consistency's sake, I'd rather remove nil.to_a if we don't see compatibility problems.

Matz.

#5 Updated by Nobuyoshi Nakada 10 months ago

  • Description updated (diff)

#6 Updated by Stephen Celis 10 months ago

=begin
Hm... (({nil.toh => {}})) is brand-new and creates an analog to (({nil.toa})), so why would we remove (({nil.to_a}))?

(({(**nil)})) would be another tool that a programmer could use when handing arguments off between methods. One can currently use a single splat to convert nil or an empty array into an empty arg list. The ability to use double-splat to convert nil, an empty array, or an empty hash into a keyword arg list is equally powerful. It means flexibility in input and in handing off and converting arguments among different methods.

For instance, there was a recent moment where I thought I could elegantly double-splat an argument passed to a method in order to convert it to a hash without using (({kwargs || {}})), similar to what I've been able to do with a single splat and an args array (or, in some cases, a nil arg). The fact that I was unable to convert the object using a double-splat (while I can convert an object using a single-splat) seemed unpredictable. Rubyists have grown to expect that a single splat converts arguments passed in a certain way. I was hoping that handling the double-splat in a similar way would be an elegant way to resolve the current dissonance.

Here's a contrived example:

def outermethod **options
inner
method(**options[:inner_options])
end

The ability to pass a value that may be a hash or may be nil as keyword arguments simplifies code (you don't have to check for nil) and makes it apparent as to what kind of value the inner method is taking (the same way a single splat denotes an args list).

I realize that keyword arguments are relatively new. I've already come up against some roadblocks using them (and finding their behavior fundamentally change between patchlevel 0 and 195). I think it's a great time to refine how we expect them to behave.

Again, the unexpected difference in behavior:

Array(nil) # => []
nil.to_a # => []
[*nil] # => []

Hash(nil) # => {}
nil.to_h # => {}
{**nil} # TypeError: no implicit conversion of nil into Hash
=end

#7 Updated by Evan Owen 10 months ago

I'm glad this is getting addressed. Handling nil values is one of the most tiring and error-prone aspects of coding in Ruby. Much has been said about the mistake of introducing NULL references in languages (http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare, among others), and there are considerable efforts being made in many places to remove much of the pain that they cause.

I've also personally run into the situation shown in stephencelis's example above, and was quite surprised and disappointed by it. I'm 100% in support of any effort to gracefully handle nil values when coercing. The consistency added by stephencelis's patch is a big win, IMO.

#8 Updated by Stephen Celis 9 months ago

Tried my best to thoughtfully address questions before: http://bugs.ruby-lang.org/issues/8507#note-6

Does anyone have any additional thoughts on the matter?

#9 Updated by Elliot Winkler 9 months ago

So let's think about why we have splat(nil). Ordinarily you wouldn't write this code like that of course, but you might have a variable foo and unbeknownst to you (or perhaps not), foo happens to be nil, so you end up with splat(nil). So I think we're getting a little distracted here by the nil.

Let's think about why we use splat at all. We use it because we have an array, and we want to call a method and spread the array across to create a list of arguments. So if foo is [1, 2, 3], splat(*foo) is splat(1, 2, 3).

Why don't we have an equivalent splat syntax for hashes? Well because we don't need it. You can just say splat(foo), assuming foo is a hash, and it just works. What would splat(*foo) even *do?

For instance say #splat is defined as:

def splat(foo: 'bar', **kwargs)
end

Now assuming foo is {foo: 'bliz', a: 'b', c: 'd'}, what would splat(**foo) do? Wouldn't this just end up calling splat(foo: 'bliz', a: 'b', c: 'd'), and therefore be the same as calling splat(foo)?

So while I agree for consistency's sake that foo(*whatever) should do something just as foo(whatever) does something, I don't really know if this syntax would be used.

#10 Updated by Stephen Celis 9 months ago

Thanks for weighing in. Given that nil is so prevalent in the language, I don't think we can ignore it.

Also, **foo does already exist in the language, and it does do something (validates that the hash acts like kwargs and raises a TypeError if any of the keys aren't symbols).

Meanwhile, what if splat were defined as:

def splat(foo = {}, **kwargs)
p foo, kwargs
end

What should happen with something like the following?

splat(foo: 'bar', a: 'b', **{c: 'd'})

Should it return:

[{}, {:foo=>'bar', :a=>'b', :c=>'d'}]

Or should it return:

[{:foo=>'bar', :a=>'b'}, {:c=>'d'}]

Is it something one can easily guess?

And what should this do?

splat(foo: 'bar', 'baz' => 'fizz', buzz: 'bucket')

Heck, what if {**hashwithstringkeys} actually did something like Active Support's symbolizekeys?

Right now, the behavior seems ambiguous in general.

#11 Updated by Hiroshi SHIBATA 3 months ago

  • Target version changed from 2.1.0 to current: 2.2.0

Also available in: Atom PDF