Project

General

Profile

Actions

Bug #19286

closed

What should kwargs' arity be?

Added by matsuda (Akira Matsuda) almost 2 years ago. Updated almost 2 years ago.

Status:
Closed
Assignee:
-
Target version:
-
ruby -v:
ruby 3.3.0dev (2022-12-28T16:43:05Z master cada537040) +YJIT [arm64-darwin21]
[ruby-core:111522]

Description

Hello, guys. It's time for a quick Ruby quiz.

Q: What is this method's arity?
def f(a:, b:)
end

It requires two arguments, hence it should be 2?
Or if we call this method with one argument, the error message says "wrong number of arguments (given 1, expected 0; required keywords: a, b) (ArgumentError)", which means the arity is 0, maybe?

A: The answer is,
$ all-ruby -e 'p method(def f(a:, b:) end).arity'
ruby-2.1.0-preview1 0
...
ruby-2.1.0 0
ruby-2.1.1 -1
ruby-2.1.2 1
...
ruby-3.1.0 1

it's been 1 since 2.1.2. But why 1? Why not 2 nor 0?

I asked this question to the ruby-core people, and ko1's answer was that even he has no idea what the number 1 means.

So I thought it'd be worth asking this question here.


Files

random_-_ruby-lang_-_Slack.png (40.1 KB) random_-_ruby-lang_-_Slack.png matsuda (Akira Matsuda), 12/30/2022 03:04 AM

Updated by hmdne (hmdne -) almost 2 years ago

kwargs are... complicated. Let me first extend the issue with additional versions of the above (I run Ruby 3.1, but from what I know, everything applies to anything >= Ruby 3.0):

[1] pry(main)> method(def a(a:, b:); end).arity
=> 1
[2] pry(main)> method(def a(a:, b: 5); end).arity
=> 1
[3] pry(main)> method(def a(a: 5, b: 5); end).arity
=> -1
[4] pry(main)> method(def a(**kwargs); end).arity
=> -1
[5] pry(main)> method(def a(**nil); end).arity
=> 0
[6] pry(main)>

So, basically, how I understand the kwargs: they are actually internally the last Hash argument of a function (that is passed with a special flag). If all kwargs are optional, then it's an optional argument (so arity is -1).

They are unlike how blocks work - it's not a separate category of arguments.

If a function is declared with kwargs arguments special syntax (later I will call it a kwargs-syntax function), then this flag is enforced while checking arity (since Ruby 3.0 I believe). But otherwise, it's not, and kwargs are just passed as a simple argument:

[8] pry(main)> def a(kwargs); kwargs; end; a(a: 5)
=> {:a=>5}
[9] pry(main)> def a(kwargs = {}); kwargs; end; a(a: 5)
=> {:a=>5}
[10] pry(main)> def a(*args); args; end; a(a: 5)
=> [{:a=>5}]
[11] pry(main)> 

It is possible to call a kwargs-syntax function with a non-kwargs Hash argument (but only with restarg syntax), if we set a flag (a distinct one, from what I understand) using Hash.ruby2_keywords_hash.

[15] pry(main)> def a(**kwargs); kwargs; end; a(*[Hash.ruby2_keywords_hash({a: 5})])
=> {:a=>5}
[16] pry(main)> 

So, in a way, the first list of examples would be equivalent in terms of arity to the following set of examples:

[1] pry(main)> method(def a(kwargs); end).arity
=> 1
[2] pry(main)> method(def a(kwargs); end).arity
=> 1
[3] pry(main)> method(def a(kwargs = {}); end).arity
=> -1
[4] pry(main)> method(def a(kwargs = {}); end).arity
=> -1
[5] pry(main)> method(def a(); end).arity
=> 0
[6] pry(main)> 

I think a better interface to get the argument signature of a function is Method#parameters:

[18] pry(main)> method(def a(kwargs); end).parameters
=> [[:req, :kwargs]]
[19] pry(main)> method(def a(**kwargs); end).parameters
=> [[:keyrest, :kwargs]]
[20] pry(main)> method(def a(a:, b:); end).parameters
=> [[:keyreq, :a], [:keyreq, :b]]
[21] pry(main)> method(def a(a: 5, b: 7); end).parameters
=> [[:key, :a], [:key, :b]]
[22] pry(main)> method(def a(kwargs = {}); end).parameters
=> [[:opt, :kwargs]]
[23] pry(main)> method(def a(**nil); end).parameters
=> [[:nokey]]
[24] pry(main)> 

(My understanding on this topic stems from a fact, that I am working on updating kwargs support in Opal to match MRI 3.x behavior)

Updated by Eregon (Benoit Daloze) almost 2 years ago

I would guess because f({a:1,b:2}) used to work before 3.0 and it was passing "one argument", hence arity 1.
Also def m(*); end can be called like m(a:1,b:2) and that's clearly one argument on the callee side.
So from the caller point of view kwargs are at most 1 argument.

In general it seems kwargs are considered as 0 (if all optional) or 1 "argument" (from a positional sense), which stems from that history.

Updated by bkuhlmann (Brooke Kuhlmann) almost 2 years ago

💡 In case it helps -- regarding the mention of method parameters above -- I've found it easy to forget how method parameters work so wrote up an entire article on usage. I also find parameters more easy to reason about than arity (despite being less performant).

Updated by mame (Yusuke Endoh) almost 2 years ago

Discussed at the dev meeting. @matz (Yukihiro Matsumoto) said the current behavior is fair, and @matsuda (Akira Matsuda) agreed with that. So keep it as is.

Updated by matsuda (Akira Matsuda) almost 2 years ago

  • Status changed from Open to Closed

Withdrawing the issue. Thank you for your participation in the discussion!

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0