-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unban and
and or
control flow operators
#730
Conversation
You make some strong arguments, but I'm still not convinced. Your best examples of their usage (under bullet # value = fetch_necessary_value or raise Error, "Pretty long error message"
value = fetch_necessary_value
raise Error, "Pretty long error message" unless value
# or :)
value || raise Error, "Pretty long error message" # value = fetch_optional_value or return
value = fetch_optional_value
return unless value
# or :)
value || return # possible_error_check || other_possible_error && condition_for_that_error and raise
problem = possible_error_check || other_possible_error && condition_for_that_error
raise if problem
# or :)
problem && raise While I'm not comfortable banning part of Ruby on principle either, I haven't seen code that was improved with |
I'll not going to comment on this right away. I'd rather like to hear what @rubocop-hq/rubocop-core members think, @pirj, and everyone else who'd like to participate of course. |
(I promise this is my last comment in the discussion to not turn it into flame, until very strong need to support my proposal with new, non-obvius arguments.)
Well, first of all, I believe that for unbanning language feature a clear showcase of its non-confusing usage is enough, this usage should not necessarily be "strictly superior" (also, "superior" for whom?..). Second, there are many styles and approaches, but I believe some general tendency towards DRYing up successive statements mentioning the same variable is an important part of lot of them. So, especially in "precondition" context of the method (preparing and validating arguments), I believe that some superiority is clearly present: # or version: we are fetching value this way, OR, if it isn't ther, we raise (something)
value = fetch_necessary_value or raise Error, "Pretty long error message"
# 2-line version: first, fetch the value. Then, next step, raise error (if something)
value = fetch_necessary_value
raise Error, "Pretty long error message" unless value
# Two problems here:
# 1. there are two statements, not one, so the relationship between them --
# that the second is the mandatory fallback for first, is not immediately obvious
# 2. I haven't wrote "Pretty long error message" accidentally.
# Imagine this:
raise OurNamespace::SomeParticularError, "Expected value to be fetched when #{context}, but there were no. Try providing value." unless value
# vs this:
value = fetch_necessary_value or raise OurNamespace::SomeParticularError, "Expected value to be fetched when #{context}, but there were no. Try providing value."
# The second version obviously gives you the clear "fetch or raise" vision, plus
# an ability to format the message as a "tail":
value = fetch_necessary_value or raise OurNamespace::SomeParticularError,
"Expected value to be fetched when #{context},"\
" but there were no. Try providing value."
# As about this:
value || raise Error, "Pretty long error message"
# I believe you are joking, because this is
# a) just meaningless on a separate line and
# b) you'd better try it first, it can't be parsed by Ruby at all I recall a dialog with a large group of very high-level colleagues on my previous work: |
I'm bad at remembering things and prefer simple code that I wouldn't have to spend much time understanding, and preferably wouldn't have to look up operator order precedence in the docs. The usage of It's not correct that the guide advises using
It's totally up to the project maintainers to turn off this or any other rule if they decide the developers working with the code on a regular basis are well aware of how exactly this works and should be used. Would you agree with softening the wording from
The use of one-liners is justified when the code is clean and readable. I'm afraid the example with a very long error message and a conditional doesn't fall into this category.
|
There are a lot of great rational arguments to be made for the use of these, and there are just as many rational arguments to be made against them. However, it doesn't matter how good it sounds in theory, if it doesn't work out in practice. The empirical argument unequivocally supports the case against. When I started out with Ruby, I routinely encountered logic errors introduced by these. In later years, I haven't encountered any at all, because their use has largely disappeared. On the other hand, so far I haven't come across a single place where they could make our code more readable or easier to understand. In other words, my experience with these, after doing Ruby full-time for half a decade or so is exactly summarized in the Style Guide:
High risk, low reward. For those reasons, I'm not too keen on recommending these to anyone. Whether the Style Guide should be updated from a "don't use" to a "use at your own risk" stance, I don't have a strong opinion. |
What if we limit the use of This is something we could easily check in RuboCop, BTW. |
OK, let me be bitter. So far, all counter-arguments can be boiled down to exactly two statements:
And (2) is exactly the problem I am trying to address. I believe that In other words, people tend to constantly confuse "I haven't tried to use it" with "it is unreadable / not clean code". The thing is (and I'll be bold here, the discussion seems to be in a dead-end anyways),
But however long I'll make the list above, the answers would be "empirical": I don't see how it is readable, I don't like how it looks, I never saw it used in production, Python doesn't have it and it would be confusing, CoffeeScript uses Unfortunately, at the end of the day, I am maybe too old and disillusioned to fight those windmills. And Ruby's dying anyways :) |
I wonder if not RuboCop is at least partially to blame for that.
On the other hand, you don’t have to look very far to find popular projects using control flow keywords as boolean operators. The first three gems I checked gave three results:
So while I may claim to always only use |
I am willing to volunteer working on this, because I think it could serve as a good educational tool that might help bridge the gap to those using |
I would dispute it. Maybe this use became obvious for experts, but it is still not obvious for nonexperts. |
Also, I would not consider
as a readable code |
@matkoniecz That is because you're doing too much in one same statement. You can make anything unreadable if you want to. At least it's clear that if # 1.
variable = something_that_might_return_falsey or return
# 2.
return unless (variable = something_that_might_return_falsey)
# 3.
variable = something_that_might_return_falsey
return unless variable In my opinion option 2 is just plain bad. Whereas option 1 and 3 are both equally readable. For this reason I would go for option 1 since it's one line less. If you follow the default RuboCop guidelines a method may only have 10 lines of content. Changing two lines into one is pretty huge if you don't sacrifice any readability. |
For me: 2 is incredibly ugly and unclear. I would save 1 style for code golf puzzles showing off tricky ruby semantics |
README.md
Outdated
if x | ||
log("extracted") | ||
return | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about mix of logical and flow operator like the following?
this.is_not_right? and raise 'error' unless may_be_raise?
Is that good or bad? If bad, what's good alternative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO, it's bad because there are too many control flow keywords there, just like:
raise 'error' if this.is_not_right? unless may_be_raise?
A good alternative:
unless may_be_raise?
this.is_not_right? and raise 'error'
end
(It seems the logic is inverted for the name may_be_raise?
)
@bbatsov Is it possible to make, 4 years later, an executive decision and acknowledge that banning of language features was an early mistake of the style guide?.. The current situation is really honestly weird. The corresponding cop currently does the following:
# Cop's example:
# good
foo.save && return
# does it look good for you?..
render 'my_template' && return # parsed as render('my_template' && return)
# does it look good for you?
user.suspended? && return :denied # Syntax error |
README.md
Outdated
`if` and `unless`; `&&` and `||` are also acceptable but less clear. | ||
<sup>[[link](#no-and-or-or)]</sup> | ||
* <a name="no-and-or-or"></a><a name="and-or-flow"></a> | ||
Distinguish between `&&`/`||` logical operators and `and`/`or` control flow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd phrase this more like a rune - e.g. Don't use and
and or
as logical operators...
@zverok Yeah, I will - let's allow them, but let's be more specific on the wording and add extra rationale (e.g. maybe a big sidebar and some note this has long been a controversial topic in the Ruby community). |
@bbatsov Thanks. I rebased and reworded the PR on the new README.adoc, please take a look 🙏 |
README.adoc
Outdated
For boolean expressions, always use `&&` and `||` instead. | ||
For flow control, use `if` and `unless`; `&&` and `||` are also acceptable but less clear. | ||
`and` and `or` are control flow operators. They have very low precedence, and should only be used | ||
as a final fallback statements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why fallback? I guess you meant final expression in a method in general, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bbatsov I tried to adjust the wording, making it more explanatory. I also moved the "good usage" example to be the first one, so it also works as a demo of the concept.
README.adoc
Outdated
baz = false || true && false # => false (it's effectively false || (true && false)) | ||
---- | ||
**** | ||
NOTE: whether organizing control flow with `and` and `or` is a good idea was a controversial topic in community for a long time. But if you do, use these operators and not `&&`/`||`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whether -> Whether
use these -> prefer these operators over
🎉 Thanks. |
Good things take a while... 😉 |
Follow up #730. The code highlight was not closed properly.
This is not good.
|
And so, after several years of private discussions, here I am!
My reasoning goes as this:
and
/or
were understood by most of the community (and previous versions of this guide) as "broken logical operators" with surprisingly low priority, and, to add insult to injury, equal priority towards each other (unlike usual higher priority of logical "and"). But, as time went and approaches to Ruby style and coding practices became crystallized, it became obvious thatand
/or
were designed for control flow, and their design is extremely suitable for this task, as shown by examples below:&&
and||
for it, which do not allow most of the good usages of flow control! It is slightly like "screwdrivers are banned; use your pocket knife to screw things".for
recommendation is softened with "unless you know exactly why". So, this "personal hostility" towards this particular feature looks obviously outdated.and
/or
that could be thought of is their equal priority. I believe that it doesn't matter at all, as for the flow control more than one flow-control operator in a row is weird anyways (and my proposed change advices on that). As a one more side note, I assume that idea of equal priority came exactly from "flow control" domain: to avoid "reordering" of flow statements, in examples like:output or log and exit
("regular" priorities would read it asoutput or (log and exit)
, so noexit
ifoutput
was successful, "flat" priorities would do(output or log) and exit
). I agree it could be thought as convoluted, and, again, advice against combining flow control operators.I believe this justifies the change proposed.
NB: I am preserving
#no-and-or-or
anchor (in addition to the new#and-or-flow
anchor), in order to old links to this rule still work (but leading to the new rule).