Issue #16463 has been updated by rafaelfranca (Rafael Fran=E7a).


This solution would be my first choice if for some reason Rails needs to as=
k users to change their code to use `ruby2_keywords`.

In our current problem, users define Mailers and those mailers have actions=
, that are public methods in the mailer class. The way Rails dispatch the a=
ctions uses a few levels of delegations and all those delegations can pass =
keyword arguments. If it was not possible to remove the warnings without as=
king users to mark their actions using `ruby2_keywords` I was going to use =
the `method_added` hook on the mailer base class to automatically call `rub=
y2_keywords` in all public methods of a mailer.

I'm not sure yet if that solution is needed at all, but I think that by mar=
king all methods with `ruby2_keywords` in 2.7 we are delaying the warnings =
and all code changes to Ruby 3. Maybe that is a good thing and more inline =
with the stability of Ruby releases but, as a library maintainer, I don't m=
ind a rocky path in order to arrive to a good destination. And, I prefer to=
 do that sooner than later.

We are being pushing this style of changes in Rails for a while. If the Rai=
ls 4 to Rails 5 upgrade path taught me something is that the more you delay=
 those breaking change, the more people will delay fixing them. It took us =
more than 1 year to upgrade the Shopify application from Rails 4.2 to Rails=
 5.0 because we deprecated behavior in Rails 4.0 but only removed it in Rai=
ls 5.0. Instead of fixing the deprecated behavior through the 3 years betwe=
en Rails 4.0 and 5.0 we left everything to be done only when it was needed =
and that had a huge price.

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

I'm being very vocal against this change not because it is annoying, but be=
cause it is without precedents, at least while I'm in the community, and in=
coherent with other decisions in the same release. I don't remember any Rub=
y upgrade since Ruby 1.9 that caused so much trouble to application and lib=
rary developers, and I didn't expected this kind of decision from the Ruby =
Core team, given their track record of stability. But, I'm used to this kin=
d of decision, since I'm taking decisions like this in the Rails Core team =
for years, and I'm happy we are choosing to go by this path now.

In my opinion, if a bumpy road to a amazing destination is what we want, we=
 should be coherent. In the same release we pushed the community to change =
a lot of code to add `ruby2_keywords` for the sake of consistency we also [=
reverted a performance related change because a few gems would need to be u=
pdated](https://bugs.ruby-lang.org/issues/16150). Not to say about the froz=
en_string_literal comments that we are pushing down to the community for a =
while and now that we were arriving closer to the date where finally we cou=
ld drop that comment we just made it obligatory for maybe more 10 years.

In summary, I prefer if we make all changes necessary to the keyword argume=
nts split sooner than later but that is coming from someone with a differen=
t opinion about stability when compared with the Ruby Core.

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

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