Closed will closed 1 year ago
I'm a big numbered parameters fan and would argue that while _1 is great for a lot of cases, _2 is pretty nice too for building or working with hashes so my suggestion is to allow 2 numbered parameters (on one line blocks only of course).
Also if it were me I'd disallow named parameters in one line blocks for consistency.
I have used _1 and _2 in one-liners. I would be annoyed with it limited to only one. Enumeration of a hash will have 2 block arguments and a 1 arg limit cuts out an entire branch of standard ruby objects. I could get behind limiting it to no more than 2 for that reason.
I'm swayed by the _2 being useful for hashes. I still personally wouldn't be sad if they're overall completely forbidden, but I hasn't considered hashes when thinking that just one was good enough.
With such a new language feature, does it makes sense to be allowing/disallowing features before they’ve had a chance to be used (and abused) in the wild? It’s hard to normalize on patterns without first giving them a chance to be experimented with in various situations.
(Fwiw, I’m not in favor of disallowing them out of the gate. Other anti language features are usually uncovered as warts when newer/better ways are added to the language. In this case, blocking a new language feature immediately after its release seems tantamount to vetoing ruby-core’s intent and vision)
As an example of "in the wild learnings" I've experienced, there are a few other considerations that my teammates have found "more important" than number of parameters or number of lines in the block:
Familiarity with the method that is receiving the block Most of the Enumerable methods are (mostly) well understand and blocks that are iterating over a collection (usually) derive their context from the name of the collection receiving the method. In those cases, named parameters typically provide less value and so numbered parameters are fine. Conversely, methods that are custom defined, or simply less commonly used are at greater risk of being unclear when using numbered parameters. (That's not to say that the method itself can't clearly provide context with its own name and so on; but I've found that non-enumerable methods usually require a higher bar before numbered parameters feel acceptable.)
Clarity of the receiver A very close corollary to 1) above is how clear the receiver is. If the receiver is named (either through a bound variable or very well known method, say ActiveRecord associations), that context is shared through to the block and numbered parameters are fitting. On the other hand, if the receiver is the last in a chain of method invocations, it is the block's parameter names that provide the context of "what do we have at this stage of the pipeline". As before, this is something that isn't dependent on block size or number of parameters.
Number of variable references
This item does typically correlate directly with block length, conversely! And surprisingly, doesn't seem consistent in the result of the rule. I've found that, in general, the more frequent the use of a variable, the more helpful numbered parameters become. Example: array_of_names.to_h { [_1, _1.titleize ] }
In scenarios where the variable is used more than once, we've found that downplaying the name of the variable makes the distinctions in its use more prominent. In this example, the {key => titleized }
structure is more clear than when using named parameters. Of course, when the block is long, or more than one parameters is used, the multiple references becomes extremely confusing very quickly. So this rule is highly dependent on short blocks.
These are an awful lot of additional rules to attempt to encode into standard. It's probably not worthwhile to attempt to do so, but all these highly contextual scenarios make me a lot more apprehensive to start applying a rule without a bit more experimentation time.
I'm sympathetic to the motivation behind the these rules. I rarely break the first rule... often enough that I disabled the rubocop rule for my projects, but I agree it's a good general rule of thumb and worthy of inclusion. But I personally don't find the second one to be very helpful.
I use the same rule for two or three or more numbered parameters as I do for one: will it be obvious to a reader what each parameter stands for, so long as they have basic knowledge of the API? I.e. perfect recall of all method signatures shouldn't be needed, but an intuition based on method names and contextual experience should suffice.
IFF the keys and values are clear within the context, I consider it okay to use _2
with any Enumerable methods on Hash. This is the same rule I use for Enumerable blocks with unary values, or with any n-tuple.
Is this more likely the case for unary blocks than for binary, trinary, n-ary? Yeah, of course it is. But unary blocks often don't pass this test either. Arguably, that's a code smell of its own. But nevertheless, although I love numbered parameters, I frequently "need" to use a named parameter for tiny unary blocks.
And that's the main issue I have with the "only use _1" rule. It replaces a simple heuristic that requires context, discretion, or discussion with a mechanistic rule that cripples a useful language feature even while it fails to block the confusing cases it was invented for.
Some examples of what I might (in a particular context) consider "good" multiple numbered parameters:
It's pretty obvious what's happening here, right?
users.each_with_index do
puts "%d: %s" % [_2, _1.email]
end
This next pattern can sometimes be replacement for--or replaced by--a "find pattern" pattern match.
ordered_sequence
.each_cons(4)
.select { _1 + _3 == fn(_2, _4) }
In a homogeneous list, there may be no more meaningful names than |a, b, c, d|
, or |first, second, third, fourth|
. Sometimes names are chosen based on their presumed role in the pattern, but that can be more confusing than just using a, b, c
, x, y, z
, _1, _2, _3
. (It reads naturally for pattern matching than for select
blocks, IMO.)
Or, how about a yacc-like DSL, where context makes it clear, and the verbosity of named params would just lead to clutter:
rules[:stmt] = stmt k_if expr { cond_if(true, _3, _1) }
rules[:stmt] |= stmt k_when expr { cond_loop(true, _3, _1) }
rules[:stmt] |= stmt k_unless expr { cond_if(false, _3, _1) }
rules[:stmt] |= stmt k_until expr { cond_loop(false, _3, _1) }
This might not be obvious to someone unfamiliar with parsers. But, it's much more readable to me than verbosely naming parameters. But, to be fair, in a DSL like the above, you'd probably need to disable many standard rules. 😉
Anyway, those are just my $0.02. 🙂
Tangentially: I have strong hope that, someday, we'll be able to use it
and/or _
as an "implicit unary block parameter" in some future version of ruby, for many of the same reasons given here: https://bugs.ruby-lang.org/issues/18980. If that were to happen in some form, this rule would basically remove numbered parameters as language feature.
Also, apologies for the verbosity, when all I really needed to say is that I agree 100% with @jasonkarns. 😉
Hi 👋 I'd like to add an argument against allowing numbered parameters.
I came to Ruby from Perl, and one thing that I really appreciated with Ruby was the abundance of natural language. Perl has a lot of magic numbers and symbols, and while I appreciate the language for other reasons, I found it often very hard to read.
Numbered parameters feels like the same kind of "magic Perl-isms" that I have to stop and think about when reading code. When everything is forced to be written out in words I find code easier to skim and quickly grasp.
It also acts as a forcing function to reconsider my design. If it's hard to find a good block parameter name then I try to change my data structure until the naming is easier.
As a consequence I never use numbered parameters when I write Ruby, and most likely never will, and try my best to never use contractions or single letter variables either.
I think Standard disallowing numbers parameters completely would be an overreaction.
I think the Rubocop default restrictions are reasonable.
I find that users.map { _1.height }
is easier to describe to new Ruby developers than users.map(&:height)
.
On reflection, I found @jasonkarns point:
I’m not in favor of disallowing them out of the gate. Other anti language features are usually uncovered as warts when newer/better ways are added to the language. In this case, blocking a new language feature immediately after its release seems tantamount to vetoing ruby-core’s intent and vision
To be especially compelling. This is a brand new feature. Most of us haven't used it. The community hasn't had much time to see in practice whether productive patterns or counter-productive antipatterns proliferate. So it'd be hubris to prevent its full use now when we know the least about its impact. Going to close.
Let's revisit when every Ruby codebase is a sunburnt hellscape marred with dozens of unnamed parameters in hundred-line long blocks
I dont disagree with closing this, makes sense. But man does it make me feel old that 3 years is considered by everyone to be brand new.
I dont disagree with closing this, makes sense. But man does it make me feel old that 3 years is considered by everyone to be brand new.
FWIW, for some legacy codebases that are slower to upgrade, the pertinent date isn't when 2.7 was released but when ruby 2.6 was EOLed, and that was less than 10 months ago. Although actively maintained gems should have fixed their forward compatibility problems with 2.7 (kwargs...) three years ago or more, very few would drop backward compatibility with 2.6 before it was EOL and many still haven't.
@will Right? Though I think there's additional lag between when the feature became available and when it started to be used. By-and-large, I would wager that it is only really used in codebases that began on ruby 2.7+. Any apps that have been upgraded to that point would likely have even further delay before its use starts to leak into the codebase. It's quite possible that my experience is skewed, but most rubyists I work with day to day have never even encountered the feature, regardless what version they're on, so there isn't much use to reflect on. 🤷
Numbered Parameters were introduced in Ruby 2.7 I think. They let you not name a parameter into a block like
.each { |x| puts x }
and instead do.each { puts _1 }
. I haven't used them a whole lot but they're kinda handy in some spots where the block is very short and it's just one parameter.Allow them or not seems like something Standard should take a position on. I wouldn't be sad if the position was "don't use them at all", but if the position is "yes" then I think the default for the the two rubcop rules I found seem reasonable: only on single line blocks, and no more than one numbered parameter.
https://rubydoc.info/gems/rubocop/1.42.0/RuboCop/Cop/Style/NumberedParameters
https://rubydoc.info/gems/rubocop/1.42.0/RuboCop/Cop/Style/NumberedParametersLimit