Project

General

Profile

Actions

Feature #17785

open

Allow named parameters to be keywords

Added by marcandre (Marc-Andre Lafortune) 7 months ago. Updated 5 months ago.

Status:
Open
Priority:
Normal
Target version:
-
[ruby-core:103305]

Description

We should allow named parameters to be keywords and use add a trailing _ to the corresponding variable:

def check(arg, class:)
  arg.is_a?(class_)
end

check(42, class: Integer) # => true

Currently, if we want such an API we have to use **rest:

def check(arg, **rest)
  class_ = rest.fetch(:class) { raise ArgumentError('missing keyword: :class')}
  if rest.size > 1
    unknown = rest.keys - [:class]
    raise ArgumentError("unknown keyword(s): :#{unknown.join(', :')})
  end

  arg.is_a?(class_)
end

This is very verbose, much less convenient, much less readable, prevents steep from generating the proper signature, etc.

We should do the same for pattern match.


Related issues

Related to Ruby master - Feature #13207: Allow keyword local variable names like `class` or `for`FeedbackActions

Updated by mame (Yusuke Endoh) 7 months ago

An interesting idea. I have never thought of it. A clearer name might be better, such as keyword_variable_class, instead of class_.

FYI, Binding#local_variable_get was introduced for the very use case.

def check(arg, class:)
  class_ = binding.local_variable_get(:class)
  arg.is_a?(class_)
end

Personally I don't like local_variable_get, though, because it is unreasonably slow, and still less convenient.

Updated by zverok (Victor Shepelev) 7 months ago

We actually can:

def check(arg, class:)
  arg.is_a?(binding.local_variable_get('class'))
end

Here nobu (Nobuyoshi Nakada) have argued that this is exactly how it is intended to be done.

I vaguely remember arguing somewhere about some syntax to access "specially named" local vars, but I don't remember what I've proposed then, and can't find the ticket :shrug:

Updated by marcandre (Marc-Andre Lafortune) 7 months ago

Clearly, class_ is much simpler and much faster than binding.local_variable_get(:class)...

Updated by byroot (Jean Boussier) 7 months ago

Arguably it's a bit of a stretch, but how would you handle: foo(class_, class:)?

What if instead of mangling the variable name, there would be a way to tell the parser to interpret the next word as a regular name rather than a keyword? e.g.:

def check(arg, class:)
  arg.is_a?(\class)
end

\ being a common escaping character I think it the one that would make the most sense.

And that would allow to make it work with regular parameters as well:

def diff(start, \end)
  \end - start
end

Even though this use case is much less important, except for documentation purposes.

Updated by Eregon (Benoit Daloze) 7 months ago

I like byroot (Jean Boussier)'s idea to solve the more general issue and not just this specific instance.

marcandre (Marc-Andre Lafortune) wrote in #note-3:

Clearly, class_ is much simpler and much faster than binding.local_variable_get(:class)...

It can be the same performance with a JIT and escape analysis (i.e., it's the same on TruffleRuby).

Updated by austin (Austin Ziegler) 7 months ago

I’ll also say I like byroot’s idea, especially as bare \VALUE
currently throws a SyntaxError.

Updated by duerst (Martin Dürst) 7 months ago

I think it's not a good idea to introduce special syntax such as class_ just for the case where arguments are named with keywords. First, the number of keywords is very low, which means that the cases where using a keyword as an argument name makes sense is also very low. Second, there are keywords such as if and else that are of very doubtful use as variable names anyway. Third, using keywords as variable names inherently increases the cognitive load on the reader and is a source for confusion.

Also, the special meaning of the trailing underscore will be difficult to recognize and understand for most people because it will appear so rarely. And the _ doesn't match well with the : in the argument list. And _ also is already allowed, so at least in theory, there's a chance of compatibility problems.

And then there's good old klass, which did the job for decades. And for those who don't like klass, there's class_. What's the problem of using class_ in the argument list if your plan is to use it in the body of the method anyway?

marcandre (Marc-Andre Lafortune) wrote in #note-3:

Clearly, class_ is much simpler and much faster than binding.local_variable_get(:class)...

What about finding something in between the two? E.g. even just introducing variable_get as an alias to binding.local_variable_get would make this easier to use. And if this really needs optimization, it could be done, too, but using a different argument name would solve the problem.

With respect to \, it reminds me of older languages (such as m4, C, and TeX) where there's a purely string-based level below (or before) the usual structured syntax. Do we want Ruby to descend to that level? Escaping exists inside strings because you don't want the range of data you can handle with a programming language to be restricted by the syntax of the language itself. Also, escaping inside strings is frequent enough for everybody, and occurs in a very similar form across a wide range of programming languages, so that every programmer knows it. Backslashes in front of keywords would be a whole different matter.

There are programming languages where there are no reserved keywords. The one I know and have used is PL/1. If not having any keywords would have been a design goal of Ruby, I'm sure Matz would have found a way to get there. But it wasn't, and I guess it isn't. And as far as I understand, this proposal doesn't get us there.

In conclusion, I think this issue chases a phantom. The trade-off (rarely used obscure syntax to solve a rarely occurring pseudo-problem) is not good. It would introduce some very rarely used edge-case syntax, and wouldn't really make the language any better.

If there are no more urgent kinds of improvements to Ruby syntax that this one, then we know Ruby is in a pretty good place!

Updated by nobu (Nobuyoshi Nakada) 7 months ago

duerst (Martin Dürst) wrote in #note-7:

What about finding something in between the two? E.g. even just introducing variable_get as an alias to binding.local_variable_get would make this easier to use. And if this really needs optimization, it could be done, too, but using a different argument name would solve the problem.

In built-in methods written in Ruby, we chose __builtin.arg!(:in) form (see timev.rb).

With respect to \, it reminds me of older languages (such as m4, C, and TeX) where there's a purely string-based level below (or before) the usual structured syntax. Do we want Ruby to descend to that level? Escaping exists inside strings because you don't want the range of data you can handle with a programming language to be restricted by the syntax of the language itself. Also, escaping inside strings is frequent enough for everybody, and occurs in a very similar form across a wide range of programming languages, so that every programmer knows it. Backslashes in front of keywords would be a whole different matter.

Agree, and backslashes will be troublesome in eval obviously.

Updated by byroot (Jean Boussier) 6 months ago

the number of keywords is very low, which means that the cases where using a keyword as an argument name makes sense is also very low.

Variable and keyword names are not purely random though, so I don't think this statistical reasoning makes that much sense. Especially since keywords reserved names that are short and popular: end, class, etc. If you define method that generate some HTML, a class: named parameters is common, if you define a method that deal with period of times, end: is common, if: is common for methods taking callbacks, etc.

I'm not for adding extra syntax, but I agree with @marcandree that binding.local_variable_get(:class) is too slow to be used in many cases.

It would be great if the parser or VM would just optimize it away, but I understand that it's currently very tricky because both #binding and Binding#local_variable_get could have been redefined.

Updated by marcandre (Marc-Andre Lafortune) 6 months ago

My main objection to local_variable_get is that it's super verbose / ugly.

How would you handle foo(class_, class:)?

Setting local class_ would not happen here. It would only happen if not shadowing an existing variable (except maybe if that other variable was also created the same way)

I like \class too.

Updated by matheusrich (Matheus Richard) 6 months ago

Since we have __method__, maybe adding something like __params__?

def check(arg, class:)
  arg.is_a?(__params__[:class])
end

check(42, class: Integer) # => true

Edit:

We would have to deal with the case where positional and keyword params have the same name too.

Actions #12

Updated by nobu (Nobuyoshi Nakada) 5 months ago

  • Related to Feature #13207: Allow keyword local variable names like `class` or `for` added

Updated by harrisonb (Harrison Bachrach) 5 months ago

This feels related to this proposal I submitted ~1.5 years ago concerning external/internal names for keyword parameters as it would solve this problem somewhat more elegantly:

# Only one possible syntax--see above proposal for alternatives
def check(arg, class class_:)
  arg.is_a?(class_)
end

check(42, class: Integer) # => true
Actions

Also available in: Atom PDF