Project

General

Profile

Actions

Misc #20441

closed

Should passing keyword args to method_name(*) be an error?

Added by ozydingo (Andrew Schwartz) 7 months ago. Updated 7 months ago.

Status:
Closed
Assignee:
-
[ruby-core:117631]

Description

In the following method:

def foo(*)
  super
end

it is apparently the intended ruby 3 behavior to pass keyword args as a positional Hash to super. I believe this is confusing and can lead to hidden and hard-to-discover bugs (e.g. #20440). Since * is meant to only represent positional args, should it be an ArgumentError to pass keyword args at all to this method? Similar to how it is an error to pass positions args to bar(**).

Updated by zverok (Victor Shepelev) 7 months ago

super just passes the arguments with EXACTLY the same signature as the method it is in has.

Whether or not super is in the method, calling method defined as foo(*) with hash-like arguments without braced will implicitly convert them to the Hash as the last positional argument, and it is unlikely to change.

What exactly has super to do with it?..

Updated by ozydingo (Andrew Schwartz) 7 months ago

Why does this conversion to a Hash occur?

I would guess for some sense of backward compatibility with gems / code written in earlier versions of Ruby. But #20440 demonstrates why this compatibility is not achieved. To be clear, I'm not arguing it should be backward compatible, and it isn't; but they why should * convert keyword args to a Hash instead of considering it an error?

super only comes into play because that's the only time you'll silently pass the converted arg in code that might not be compatible with doing so, such as in the linked example. Without super the args are simply unused.

Updated by zverok (Victor Shepelev) 7 months ago

Why does this conversion to a Hash occur?

Because of backward compatibility, indeed. Even now, most of, say, Rails code still uses “old” conventions:

def foo(arg1, arg2, options = {})
  # ...
end

# and expects it to be called as
foo("bar", 1, opt1: val1, opt2: val3)

Prohibiting this (and requiring to pass to such methods only explicit hashes like foo("bar", 1, {opt1: val1, opt2: val3})) would break an enormous amount of code.

So, the last hash-like pack of arguments would be treated as keyword args for methods with them in the signature and as a last positional hash for methods without them.

The question of “generic delegation” in such conditions (with super or otherwise) is a subtle one, but it is mostly solved and requires following only two rules, as was explained in the previous ticket:

  • For Ruby 3+, to “delegate all possible kinds of arguments”, use foo(*, **)
  • If the code needs to be compatible with Ruby < 3, use ruby2_keywords

If we do neither, the method defined as foo(*) wouldn’t try to guess how to convert its arguments back to positional+keyword on super or other forms of delegation and would simply consider them all positional.

Updated by ozydingo (Andrew Schwartz) 7 months ago

Understanding better the role of ruby2_keywords is helping, thank you. It seemed to me that either way some compatibility was broken, but the subtleties of maintaining compatibility as well as possible in a variety of circumstances is indeed tricky. I appreciate your time explaining it further here.

Actions #5

Updated by alanwu (Alan Wu) 7 months ago

  • Status changed from Open to Closed
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0