Project

General

Profile

Feature #16166

Remove exceptional treatment of *foo when it is the sole block parameter

Added by sawa (Tsuyoshi Sawada) 3 months ago. Updated 10 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:94925]

Description

In the parameter signature of a code block for a method that is not involved in method definition or creation of lambda objects, two types of arguments ["a"] and "a" are neutralized:

instance_exec(["a"]){|foo, bar| foo} # => "a"
instance_exec("a"){|foo, bar| foo} # => "a"

instance_exec(["a"]){|*foo, **bar| foo} # => ["a"]
instance_exec("a"){|*foo, **bar| foo} # => ["a"]

This is the same behavior as with assignment constructions:

foo, bar = ["a"]; foo # => "a"
foo, bar = "a"; foo # => "a"

*foo = ["a"]; foo # => ["a"]
*foo = "a"; foo # => ["a"]

And it contrasts with constructions involved in method definition or creation of lambda objects, where the distinction is preserved:

lambda{|foo| foo}.call(["a"]) # => ["a"]
lambda{|foo| foo}.call("a") # => "a"

->(foo){foo}.call(["a"]) # => ["a"]
->(foo){foo}.call("a") # => "a"

lambda{|*foo| foo}.call(["a"]) # => [["a"]]
lambda{|*foo| foo}.call("a") # => ["a"]

->(*foo){foo}.call(["a"]) # => [["a"]]
->(*foo){foo}.call("a") # => ["a"]

However, when *foo is the sole parameter of a code block for a method that is not involved in method definition or creation of lambda objects, ["a"] and "a" are not neutralized:

instance_exec(["a"]){|*foo| foo} # => [["a"]]
instance_exec("a"){|*foo| foo} # => ["a"]

behaving in contrast to assignment constructions, and rather on a par with constructions involved in method definition or creation of lambda objects.

Particularly, existence or absence of another parameter **bar entirely changes what foo refers to:

instance_exec(["a"]){|*foo| foo} # => [["a"]]
instance_exec(["a"]){|*foo, **bar| foo} # => ["a"]

I find this behavior inconsistent and confusing. I would like to request to remove this exceptional treatment of splatted parameter *foo when it is the sole parameter in a code block. I request this behavior:

instance_exec(["a"]){|*foo| foo} # => ["a"]

History

#1

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Description updated (diff)
#2

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Description updated (diff)
  • Subject changed from Remove exceptional handling of *foo when it is the sole block parameter to Remove exceptional treatment of *foo when it is the sole block parameter

Updated by mame (Yusuke Endoh) 3 months ago

I agree that Ruby's arguments are insanely complex. In the basic case, "a" and ["a"] are distinguished.

p instance_exec("a")  {|foo| foo } #=> "a"
p instance_exec(["a"]){|foo| foo } #=> ["a"]

p instance_exec("a")  {|*foo| foo } #=> ["a"]
p instance_exec(["a"]){|*foo| foo } #=> [["a"]]

In some cases, they are not distinguished.

p instance_exec("a")  {|foo, bar| foo } #=> "a"
p instance_exec(["a"]){|foo, bar| foo } #=> "a"

p instance_exec(["a"]){|*foo, **bar| foo } #=> ["a"]
p instance_exec("a")  {|*foo, **bar| foo } #=> ["a"]

The rule is fairly complex or even inconsistent. I cannot understand the condition.

I have no opinion which case |*foo| should belong to. (I personally hope that |*foo, **bar| belongs to the same case as |*foo| because keywords are separated from positional arguments.)

Anyway, I don't think that it is a good idea to change the behavior just because it is inconsistent. We need an evidence that the behavior actually confuses many people, at least.

Updated by shevegen (Robert A. Heiler) 3 months ago

We need an evidence that the behavior actually confuses
many people, at least.

It does not confuse me because ... I try to avoid it altogether. :D

I think sawa's issue can be a bit shortened (sorry!) to the last
comparison:

instance_exec(["a"]){|*foo| foo} # => [["a"]]
instance_exec(["a"]){|*foo| foo} # => ["a"]

Although I may miss (or not completely understand) all of the reasoning,
I think that change would make sense (to me) - but I may not understand
the consequences.

I only remember even matz having fun in a presentation with the
whole keyword arg situation before. ;) (One reason why I try to
actually avoid keywords is because I find them more difficult to
deal/cope with than oldschool options hash. But I guess this may
differ from ruby user to ruby user since it is a personal preference.)

Perhaps there should be a simple and consistent rule for how * and **
is to be interpreted at all times, including what should happen if
both are used at the same time. What I find indeed a bit confusing
is that * changes if ** is also used. That part is very strange to
me personally. Might also be mentioned in the documentation, but
for me personally, I gladly stick to the simpler variants. :D

Updated by shevegen (Robert A. Heiler) 3 months ago

Actually that reminds me - mame mentioned that the ruby core team needs a
motivation/impetus if a change is necessary based on real usage. So I think
this may be a good call for ruby users to comment in particular when it
may affect them (either way) in actual code. Me personally I am not affected
in either way, but it may be a good idea to get a survey/query to ruby users
out there to comment, in particular when it may affect them in their own
code base or a code base they use/depend on.

Updated by matz (Yukihiro Matsumoto) 3 months ago

I think the following code behavior is wrong:

p instance_exec(["a"]){|*foo, **bar| foo } #=> ["a"]

It should return [["a"]].

Matz.

Updated by jeremyevans0 (Jeremy Evans) 3 months ago

matz (Yukihiro Matsumoto) wrote:

I think the following code behavior is wrong:

p instance_exec(["a"]){|*foo, **bar| foo } #=> ["a"]

It should return [["a"]].

Here's a pull request for that: https://github.com/ruby/ruby/pull/2502

Note that it breaks some tests/specs. I believe the reason methods with keywords were handled differently is because the last element of the argument could be used as keywords:

$ ruby -e "p proc{|*foo, **bar| [foo, bar]}.call([1, {a: 1}])"
-e:1: warning: The last argument is used as the keyword parameter
-e:1: warning: for `call' defined here
[[1], {:a=>1}]

As you can see, this now raises a warning in Ruby 2.7, and behavior will change in Ruby 3.0. Do we want to make this change in 2.7, or do we want to wait for 3.0?

Updated by Dan0042 (Daniel DeLorme) about 2 months ago

I think this is related:

proc{ |a,b| [a,b] }.call(1,2)   #=> [1, 2]
proc{ |*ab| ab    }.call(1,2)   #=> [1, 2]
proc{ |a,b| [a,b] }.call([1,2]) #=> [1, 2]
proc{ |*ab| ab    }.call([1,2]) #=> [[1, 2]]

I really think the last result should be [1, 2]. Otherwise I totally fail to understand the logic. But of course there's always backward compatibility to worry about...

#9

Updated by Eregon (Benoit Daloze) 15 days ago

sawa (Tsuyoshi Sawada) wrote this in the dev-meeting ticket:

Unintended arity. This must be fixed in an earlier stage before Ruby 3.

I think matz conclusion is all behavior shown in this bug so far is intended, except for *foo, **bar.

Dan0042 (Daniel DeLorme)

proc{ |a,b| [a,b] }.call(1,2)   #=> [1, 2]
proc{ |*ab| ab    }.call(1,2)   #=> [1, 2]
proc{ |a,b| [a,b] }.call([1,2]) #=> [1, 2]
proc{ |*ab| ab    }.call([1,2]) #=> [[1, 2]]

That's just how Proc works, multiple parameters will splat an Array if a single Array argument is given.
A single parameter will not splat. The *rest parameter will not splat either, or it would delegate arguments incorrectly.

I think long-term we might want to use lambda semantics by default for blocks, which doesn't have that splatting magic.

Updated by Dan0042 (Daniel DeLorme) 10 days ago

Eregon (Benoit Daloze) wrote:

The *rest parameter will not splat either, or it would delegate arguments incorrectly.

Thanks! Finally I can see some meaning behind the madness. Normally I would expect proc{ |*a| }.call([1,2]) to behave like the assignment *a = [1,2] but it does not. But now I can see that proc{ |*a| foo(*a) }.call(arg1) would not work if arg1 happened to be an array.

However I still agree with sawa and believe that the current behavior for proc{ |*a| } is confusing. If you want that particular case of delegation to work you could simply use a lambda instead of a proc. Having that special exception where a proc behaves like a lambda just for |*rest|... it makes things overly complicated. I mean, even mame says he cannot understand the condition! With all due respect to Matz, this would be much simpler if we could say that all procs behave with assignment semantics, and lambdas with parameter semantics.

Of course the backward compatibility is an issue but that can be handled with proper deprecation warnings. I think that would be a worthwhile change for ruby. My 2ยข.

Also available in: Atom PDF