Issue #15078 has been updated by marcandre (Marc-Andre Lafortune).

Assignee changed from nobu (Nobuyoshi Nakada) to matz (Yukihiro Matsumoto)

mame (Yusuke Endoh) wrote:
> I'd like to agree with you, but also really like to fix #15052. Do you find a good semantics of **hash to satisfy both this ticket and that one?

This is a long post, sorry.

* Summary *

I agree that in your "two hash" example, `foo({}, **{})` should return `[{}, {}]`. I believe it is possible to have semantics that allow that. I am proposing such a semantic below that would have the following consequences:

```
h = {x: 42}
def with_keyword(*args, **options)
  [args, options]
end

with_keyword(h, **{}) # => [[h], {}] (as in 15052, currently [[], h])   

def no_keyword(*args)
  args
end
no_keyword(h, **{}) # => [h] (currently: [h, {}])
```

The current behavior is actually problematic, in particular with full forwarding (of the type `*args, **options`), while I believe my proposal fixes it.
This paves the way for a stricter handling of keyword parameters (like your proposal #14183).

* Proposed semantics *

Ruby 2.x will:
1) promote the last positional argument as keyword argument whenever possible (as is currently does)
2) demote keywords arguments to a positional argument if needed (as is currently does, but maybe differently for empty case)

In detail:
1) When calling `method(a, b, last)`, then `last` will be promoted to a keyword argument if possible, i.e. if:
  a) `method` takes keyword arguments (any of `key:`, `key: val`, or `**options` in signature)
  b) and all mandatory positional arguments of `method` are provided (here by `a` and `b`)
  c) and `last` is hash-like (i.e. `responds_to? :to_hash`)
  d) and all keys  of `last.to_hash` are symbols

Otherwise, `last` will remain a positional argument.
This is current behavior and should remain unchanged in Ruby 2.x, maybe open for removal in Ruby 3.

2) When calling `method(a, b, key: value)` or `method(a, b, **hash)` or a combination of these, the keyword arguments (here `{key: value}` or `hash`) will be demoted to a positional argument if needed, i.e. 
  a) if `method` does not accept keyword arguments 
  b) and they are non empty

This is slightly different than currently in some cases for `**{}`.

* Limitation *

While your example in "two hash" example would still work, it can't be forwarded with the naive approach:

```
def with_keyword(*args, **options)
  [args, options]
end

def forward(*args)
  with_keyword(*args)
end

forward(h, **{}) # => [[], h]   not same as with_keyword(h, **{}) # => [[h], {}]
```

Only forwarding with the full approach would work.

I believe that very few real life cases would be problematic. If your proposal is accepted, or half of it like I recommend, this won't be an issue.

* The future *

I think the reason why we haven't had too many bug reports with `**{}` yet is that the usual generic ways of forwarding are either the `delegate` library or `ActiveSupport`'s `delegate :method, to: receiver`.

Both currently use the naive forwarding (`*args`) instead of full forwarding (`*args, **options`). So neither actually deal with `**{}`.

The naive forwarding relies on promotion of last positional argument to keyword arguments. Before we can remove that automatic promotion, we need to make sure that full forwarding works well.

If `**{}` creates a positional argument, this makes any normal method with optional arguments non compatible with full forwarding! For example:

```
def multiply(*nbs)
  nbs.inject(1, :*)
end

def forward(*a, **o)
  multiply(*a, **o)
end

forward(1, 2, 3) # => should call multiply(1, 2, 3) but currently calls multiply(1, 2, 3, {}) which raises error.
```

I believe it is absolutely imperative that the code above works, in Ruby 2.x and 3.x.

For that reason I believe that `**{}` should never create a positional argument.

----------------------------------------
Bug #15078: Hash splat of empty hash should not create a positional argument.
https://bugs.ruby-lang.org/issues/15078#change-73945

* Author: marcandre (Marc-Andre Lafortune)
* Status: Open
* Priority: Normal
* Assignee: matz (Yukihiro Matsumoto)
* Target version: 
* ruby -v: ruby 2.6.0dev (2018-08-27 trunk 64545) [x86_64-darwin15]
* Backport: 2.3: UNKNOWN, 2.4: UNKNOWN, 2.5: UNKNOWN
----------------------------------------
Looks like #10856 is not completely fixed, but I can't reopen it

```
def foo(*args); args; end
foo(**{}) # => []
foo(**Hash.new) # => [{}], should be []
```



-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>