Issue #16511 has been reported by Dan0042 (Daniel DeLorme).

----------------------------------------
Feature #16511: Subclass of Hash for keyword arguments
https://bugs.ruby-lang.org/issues/16511

* Author: Dan0042 (Daniel DeLorme)
* Status: Open
* Priority: Normal
* Assignee: 
* Target version: 
----------------------------------------
As an alternative to #16463 and #16494 I'd like to propose this approach, which I believe allows a **much** more flexible path for migration of keyword arguments.

The idea is to have a subclass of Hash (let's name it "KwHash") which provides a clean, object-oriented design with various benefits.

I'll try to describe the idea by breaking it down into figurative steps. Imagine starting with ruby 2.6 and then:

### Step 1

When a double-splat or a brace-less hash is used, instead of a Hash it creates a KwHash.

```ruby
def foo(x) x end
foo(k:1).class      #=> KwHash
foo(**hash).class   #=> KwHash
[k:1].last.class    #=> KwHash
[**hash].last.class #=> KwHash
{**hash}.class      #=> Hash
```

At this point we haven't introduced any real change. Everything that worked before is still working the same way, with the ONLY exception being code like `kw.class == Hash` which now returns false. But no one actually writes code like that; it's always `kw.is_a?(Hash)`, which still returns true.

### Step 2

When there is ambiguity due to optional vs keyword argument, we rely on the last argument being Hash or KwHash to disambiguate.

```ruby
def foo(x=nil, **kw)
  [x,kw]
end
foo({k:1}) #=> [{k:1},{}]
foo(k:1)   #=> [nil,{k:1}]
```

This is the _minimum_ amount of incompatibility required to solve ALL bugs previously reported with keyword arguments. (#8040, #8316, #9898, #10856, #11236, #11967, #12104, #12717, #12821, #13336, #13647, #14130, etc.) 

### Step 3

Introduce additional incompatibility to improve clarity of design. Here we deprecate the automatic conversion of Hash to keyword argument; only KwHash is accepted. And always use the last KwHash argument if the method supports keyword arguments. With a deprecation/warning phase, of course. But importantly, all the changes required to silence these warnings are _compatible with 2.6_.

```ruby
def foo(x, **kw); end
foo(k:1)      # ArgumentError because x not specified
foo(1, {k:1}) # ArgumentError because too many arguments; Hash cannot be converted to KwHashs
opts = [k:1].first
foo(opts)     # opts is a KwHash therefore used as keyword argument; ArgumentError because x not specified
foo(1, opts)  # opts is a KwHash therefore used as keyword argument
```

At this point we have achieved _full_ **dynamic** keyword separation, as opposed to the current _almost-full_ **static** approach. I want to make the point here that, yes, keyword arguments **are** separated, it's just a different paradigm. With static separation, a keyword argument is defined lexically by a double-splat. With dynamic separation, a keyword argument is when the last argument is a KwHash.

Any form of delegation works with no change required. This preserves the behavior of 2.6 but only for KwHash objects. This is similar to having 2.7 with `ruby2_keywords` enabled by default. But also different in some ways. _Most importantly_, it allows the case shown in #16494 to work by default:

```ruby
array = [x:1]
array.push(x:2)
array.map{ |x:| x } #=> [1,2]
```

The current approach does not allow this to work at all. The solution proposed in #16494 has all the same flaws as Hash-based keyword arguments; what happens to `each{ |x=nil,**kw| }` ? The subclass-based solution allows a KwHash to be converted to... keywords. Very unsurprising.

Given that ruby is a dynamically-typed language I feel that dynamic typing of keywords if a more natural fit than static typing. But I realize that many disagree with that, which is why we continue to...

### Step 4

Introduce additional incompatibility to reach static/lexical separation of keyword arguments. Here we require that even a KwHash should be passed with a double-splat in order to qualify as a keyword argument.

```ruby
def bar(**kw)
end
def foo(**kw)
  bar(kw)   #=> error; KwHash passed without **
  bar(**kw) #=> ok
end
```

At this point we've reached the same behavior as 2.7. Delegation needs to be fixed, but as we know the changes required to silence these warnings are **not** compatible with 2.6. So here we introduce a way to _silence **only** these "Step 4" warnings_, for people who need to remain compatible with 2.6. And we keep them as warnings instead of errors until ruby 2.6 is EOL.

So instead of having to update a bunch of places with `ruby2_keywords` right now, it's a single flag like `Warning[:ruby3_keywords]`. Once ruby 2.6 is EOL these become controlled by `Warning[:deprecated]` which tells people they **have** to fix their code. Which is just like the eventual deprecation of `ruby2_keywords`, just without the busy work of adding `ruby2_keywords` statements in the first place.

The question remains of how to handle #16494 here. Either disallow it entirely, but I think that would be a shame. Or just like #16494 suggests, allow hash unpacking in non-lambda Proc. Except that now it can be a KwHash instead of a Hash, which at least preserves dynamic keyword separation.

## Putting it all together

The idea is _not_ to reimplement keyword argument separation; all that is needed is to implement the things above that are not in 2.7:
* Create a KwHash object when a double-splat is used.
* If a warning is due to a KwHash instead of a Hash, make it a different kind of warning that can be toggled off separately from the Hash warnings (and that will stay as warnings until 2.6 is EOL)

I think that's all, really...

### Pros
* Cleaner way to solve #16494
* Better compatibility (at least until 2.6 is EOL)
   * delegation
   * storing an argument list that ends with a KwHash
   * destructuring iteration (#16494)
* We can avoid the "unfortunate corner case" as described in the [release notes](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/)
   * in 2.7 only do not output "Step 4" warnings, leave delegation like it was
   * in 2.8 the "Step 3" warnings have been fixed and a Hash will not be converted to keyword arguments
   * delegation can now safely be fixed to use the `**` syntax
* ruby2_keywords is not required, which is desirable because
   * it's a hidden flag _hack_
   * it requires to change the code now, and change it _again_ when ruby2_keywords is deprecated; twice the work; twice the gem upgrades
   * it was supposed to be used only for people who need to support 2.6 or below, but it's being misunderstood as an acceptable way to fix delegation in general
   * there's the non-zero risk that ruby2_keywords will never be removed, leaving us with a permanent "hack mode"
      * dynamic keywords are by far preferable to supporting ruby2_keywords forever
* Likely _better performance_, as the KwHash class can be optimized specifically for the characteristics of keyword arguments.
* More flexible migration
   * Allow more time to upgrade the hard stuff in Step 4
   * Can reach the _same_ goal as the current static approach
   * Larger "support zone" https://xkcd.com/2224/
   * Instead of wide-ranging incompatibilities all at once, there's the _possibility_ of making it finer-grained and more gradual
      * rubyists can _choose_ to migrate all at once or in smaller chunks
   * It hedges the risks by keeping more possibilities open for now.
   * It allows to cop-out at Step 3 if Step 4 turns out too hard because it breaks too much stuff

### Cons
* It allows to cop-out at Step 3 if Step 4 turns out too hard because it breaks too much stuff




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