Issue #16463 has been updated by Eregon (Benoit Daloze).


rafaelfranca (Rafael Fran=E7a) wrote:
> This solution would be my first choice if for some reason Rails needs to =
ask users to change their code to use `ruby2_keywords`.

Nice to hear.
I wonder if Rails and other gems really need to go through adding `ruby2_ke=
ywords` which obviously is not trivial as seen in that example.
My impression in this issue is explicit `ruby2_keywords` gains little to no=
thing, but requires lots of changes in gems to fix the warnings.

> I'm not sure yet if that solution is needed at all, but I think that by m=
arking all methods with `ruby2_keywords` in 2.7 we are delaying the warning=
s and all code changes to Ruby 3.

The tricky bit here is the way recommended in the [official blog post](http=
s://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keywo=
rd-arguments-in-ruby-3-0/), that is adding `ruby2_keywords` for all delegat=
ing methods, will break in Ruby 3.clean.
And at the same time, there is no easy way to prepare for it, for instance =
with a version check because neither `*args`-delegation nor `*args, **kwarg=
s`-delegation works in Ruby 2.7.0.
`*args, **kwargs`-delegation will work in Ruby 3.0+ but it doesn't work cor=
rectly in Ruby <=3D 2.7.

With my proposition, we could simplify this whole mess and keep delegation =
code as it is until Ruby 3.warn.
And then change to `*args, **kwargs`-delegation if Ruby 2.x support is not =
needed (should be very likely at that time, that's why the plan is to do th=
is after Ruby 2.7 EOL), or to a version check approach otherwise, like:

```ruby
if RUBY_VERSION < "3"
  # To work with Ruby 2.7.0, this needs an explicit ruby2_keywords
  def delegate(*args, &block)
    target(*args, &block)
  end
else
  def delegate(*args, **kwargs, &block)
    target(*args, **kwargs, &block)
  end
end
```

> I think by delaying the warnings we will fall in the same trap we did wit=
h Rails 5.0. People will only fix the warnings after Ruby 3 is released. If=
 we think that Ruby 3 will have more changes that will require user to chan=
ge their code, we are just piling up more work to the users at the same tim=
e, what will delay their upgrades and give the impression that upgrading to=
 Ruby 3 is too hard. If we split the path to upgrade to Ruby 3 in many bump=
s in our road, it will be still bumpy, but it will at least be passable.

The problem here is `*args, **kwargs`-delegation cannot be used alone as lo=
ng as Ruby 2.x needs to be supported.
So it's needed to be a 2 phases deprecation if we want to avoid many versio=
n checks in library code:

1. Warnings about calling methods accepting keyword arguments without `**` =
or `key: value`, in Ruby 2.7.
2. Warnings about delegation changing from `*args` to `*args, **kwargs` in =
Ruby 3.warn.

But with the 2.7.0 release we actually get unhelpful warnings for delegatio=
n, so basically a third not needed step, already in 2.7 while we can't actu=
ally use Ruby 3-style delegation yet without breaking Ruby 2.x support.

> In summary, I prefer if we make all changes necessary to the keyword argu=
ments split sooner than later but that is coming from someone with a differ=
ent opinion about stability when compared with the Ruby Core.

I argued for that, which would mean removing `ruby2_keywords` in Ruby 3.0.
The rest of ruby core seemed to disagree because it would mean a lot of ver=
sion checks around delegation methods (to keep supporting Ruby 2.x).


----------------------------------------
Feature #16463: Fixing *args-delegation in Ruby 2.7: ruby2_keywords semanti=
cs by default in 2.7.1
https://bugs.ruby-lang.org/issues/16463#change-83490

* Author: Eregon (Benoit Daloze)
* Status: Open
* Priority: Normal
* Assignee: =

* Target version: =

----------------------------------------
Ruby 2.7.0 is out.
It aims to warn for every keyword argument change that will happen in Ruby =
3.0.
Most warnings are useful: adding `**`, etc is needed to not break code when=
 migrating to 3.0.

Ruby 2.7 also aims at remaining compatible with 2.6.
However there is a big breaking change here: __`*args`-delegation broke in =
Ruby 2.7 for keyword arguments__.
The workaround is adding `ruby2_keywords` to the method/block receiving the=
 keywords arguments to delegate later on.

But is it needed or useful at all to require everyone to add `ruby2_keyword=
s` in many places of their codebase?
And for rubyists to get major headaches as to why `*args`-delegation broke =
and instead has strange semantics in Ruby 2.7?
Was it useful to break delegation in Ruby 2.7?

I think not, and here I propose a solution to keep delegation in 2.7 compat=
ible with 2.6 (just use `*args` as before).

---

First I'll introduce some context.
The end goal is to have [separation of positional and keyword arguments](ht=
tps://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-key=
word-arguments-in-ruby-3-0/).
However, this will not happen in 3.0, because as long as `ruby2_keyword` ex=
ist, the separation will only be partial.
For example, `foo(*args)` should only pass positional arguments, never keyw=
ord arguments, but this can only be guaranteed once `ruby2_keyword` is remo=
ved.

The plan to get there, as far as I heard and imagine it is:
* In Ruby release 3.warn (around Ruby 2.7 EOL, maybe 3.3?), warn for every =
usage of `ruby2_keywords`, mentioning it should be replaced by `*args, **kw=
args`-delegation (or `...`, but that's severely restricted currently: #1637=
8). `*args, **kwargs`-delegation is only correct in Ruby 3.0+ so at that po=
int Ruby 2.x support needs to be dropped, or a version check be used.
* In Ruby release 3.clean (that is 3.(warn+1), maybe 3.4?), remove `ruby2_k=
eywords`. At that point, the separation of positional and keyword arguments=
 is finally achieved. `foo(*args)` will always mean "pass only positional a=
rguments". Everytime keyword arguments are passed it will be explicit (`foo=
(**kwargs)` or `foo(key: value)`), no more magic and a clean separation.

So no matter what, to get the clean separation we'll have to wait many (5?)=
 years for Ruby 3.clean, and delegation code will need to change in 3.warn.

But right now, we broke delegation in 2.7 and require to add `ruby2_keyword=
s` (which means __changing twice delegation code__ in this period) for seem=
ingly little to no benefit.

---

My proposition is to simply use ruby2_keywords semantics for all methods an=
d blocks in Ruby 2.7 (and until version 3.warn). This would be compatible w=
ith Ruby 2.6 and before.
This means, no explicit `ruby2_keywords` anywhere, no need to change anythi=
ng for delegation to work in Ruby 2.7, 3.0, ... until Ruby 3.clean.

Importantly, it means __only change delegation code once__ and __Ruby 2.0 u=
ntil Ruby.warn keep `*args`-delegation compatible and working__.

The semantics of that are (same as if `ruby2_keywords` was applied to all m=
ethods):
* When passing keyword arguments syntactically (using either `foo(**kwargs)=
` or `foo(key: value)`) to a method not accepting keyword arguments (e.g., =
`def m(*args)`), flag the keyword arguments Hash as "keyword arguments".
* Whenever calling a method with a `*rest` argument and no keyword argument=
s (e.g., `foo(*args)`), if the last argument is flagged as "keyword argumen=
ts", pass them as keyword arguments.
  If the called method doesn't accept keyword arguments, pass the Hash as p=
ositional argument and keep the "keyword arguments" flag.

That way, code like this just keeps working:
```ruby
def target(*args, **kwargs)
  [args, kwargs]
end

def delegate(*args, &block)
  target(*args, &block)
end

target(1, b: 2) # =3D> [[1], {b: 2}] in Ruby 2 & 3
delegate(1, b: 2) # =3D> [[1], {b: 2}] in Ruby 2 & 3, no warning in 2.7 bec=
ause {b: 2} is passed as keyword arguments to target
```

And also if `args` is stored somewhere or delegated multiple levels down.

Do we lose anything by not marking delegation methods with `ruby2_keywords`?
I think we lose nothing, and we gain a lot (compatibility and avoiding need=
less ugly changes).
In Ruby 3.warn we can easily warn for every case that passes keyword argume=
nts using `foo(*args)` and even have a debug mode telling where the Hash wa=
s flagged as a "keyword Hash".

Thoughts?
Should we fix delegation in Ruby 2.7 .. Ruby 3.warn so it works again and n=
ot needlessly break Ruby code? I believe YES!

---

P.S.: I actually proposed this idea on the ruby-core Slack on 13th December=
, but got just one response from @jeremyevans0:

> Me: If we applied `ruby2_keywords` automatically on all methods, would `*=
args`-delegation just keep working in 2.7 and later? I think the fundamenta=
l issue with kwargs changes is that we break *args by changing its meaning,=
 in a way it no longer works to delegate "all arguments except block". Prob=
ably almost every method that takes `(*args)` and then call some methods wi=
th `*args` intents to pass positional and kwargs as-is, no matter the Ruby =
version. If we could save this pattern we'd make the transition much nicer.
> Jeremy: I worked on a branch with `ruby2_keywords`  behavior by default (=
for all methods taking `*args`, not just those that delegate `*args` inside=
 the method: https://github.com/jeremyevans/ruby/tree/ruby2_keywords-by-def=
ault . I don't recommend that approach, as it is much more likely to result=
 in a keyword-flag hashed being created to a method where the hash should b=
e treated as positional.
> Me: Does it matter if the Hash is flagged and passed to a method not taki=
ng kwargs? It would still be the same behavior, no?
> Jeremy: You can end up with the hash being passed as keywords when you ex=
pect it to be passed as non-keywords.  It's not safe in general unless you =
know the method will be used for argument delegation.

Jeremy's concern is sometimes you might want `foo(*args)`, with `args[-1]` =
a Hash with a "keyword arguments" flag, to pass as positional to `def foo(*=
args, **kwargs)`.
However, that seems extremely unlikely to me, and not worth breaking delega=
tion in Ruby 2.7.
To have the "keyword arguments" flag, the Hash must have been passed origin=
ally as keyword arguments. It sounds unlikely you would then want to pass i=
t as positional to a method taking keyword arguments.
If you do want that, it's always possible to do `foo(*args, **{})`, which a=
lso works in Ruby 2.6 (and before).



-- =

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

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