Difference between unnecessary and dangerous parenthesis?

So, I'm learning Rust, at some point soon i'm gonna sit down and read the manual cover to cover but for now I'm bumbling through by guesswork. I am having a problem, with parenthesis.

I used to write Perl, a language with a very unpredictable order of operations. This gave me the lasting habit of using lots of unnecessary parenthesis. I love unnecessary parenthesis. So my main.rs starts with:

#![allow(unused_parens)]

…and I've been putting parenthesis around my if statements, like if (true). Early on I tried putting a match {} inside of an if conditional, where the parenthesis would have been necessary, so I figured it was best to use parenthesis always to avoid confusion like that. But then today this backfired, when I wrote if (let x = ...)which does not work.

Now I'm freaking out a little bit about the parenthesis! I feel like putting parenthesis around the if clause is not just unnecessary, but dangerous, because it put me in a situation where I was likely to make a bug. I wasn't expecting the parenthesis to have that semantic weight— I assumed parenthesis are "free". But here it wasn't, because (of course) "if" and "if let" are just two different tokens. But now I'm worrying, what other places are there where using parenthesis thoughtlessly could bite me? Are there other places where parentheses have "semantic" effect?

Could there be, or would it make sense for there to be, two different warning levels for parenthesis— warn when unnecessary, or warn when I'm about to put myself into a trap?

"Enable clippy" or just "get over yourself and don't use unnecessary parenthesis" are reasonable answers here.

2 Likes

Well, depending on your typing patterns, if you decide to put parenthesis around the last argument to a function call, you might accidentally leave your trailing comma inside the parenthesis, making a 1-tuple instead of the value you intended:

some_fn(
    val1,
    val2,
    (val3),
);

In contrast with

some_fn(
    val1,
    val2,
    (val3,)
);
1 Like

It sounds like you've built up some bad habits from experience in other languages and now those bad habits are causing you grief.

From your description, using unnecessary parentheses has caused you to conflate parentheses used for valid semantic reasons (e.g. order of arithmetic operations) with cosmetic reasons (e.g. to make order of operations really obvious to yourself). So now you don't know when parentheses are actually needed to alter the semantics of a piece of code.

It'll be hard at first, but I'd really suggest removing the #![allow(unused_parens)] and listening to the warnings. Rust has a much more predictable grammar so you just don't run into the same issues you get in Perl.

There's also while let... probably other pairs of keywords too.

Tuples are a big one. These are different:

let closure_1 = |x, y| x + y;
let closure_2 = |(x, y)| x + y;
    
println!("closure_1 takes two arguments: {}", closure_1(21, 21));
println!("closure_2 takes one 2-tuple: {}", closure_2( (81, 18) ));
    
// Error: this function takes 2 arguments but 1 argument was supplied
// closure_1((0, 0));
// Error: this function takes 1 argument but 2 arguments were supplied
// closure_2(0, 0);

Not all unnecessary parenthesis are flagged, incidentally, I think it's just "top level" ones -- around an entire expression or type. E.g. let x = 1 + (2 + 3); doesn't trigger it. So it may be worth trying to adapt... although it does require some adjustment when switching to another language and back, for me anyway, heh.

(my $rust = $perl) =~ s/\((.*)\)/$1/; # Surely always valid

Rust also cares about quality error messages though, so I imagine they'll try to detect this situation in the future due to your bug and improve the error messages.

You can match without parens too:

if match Some(0) {
    None => true,
    Some(_) => true,
} {
    println!("So true");
}

You were using the semantics of parenthesis is Perl to force the order of evaluation of expressions, which may have been different without. Those Perl parenthesis are laden with semantic significance.

It's not clear why one would assume that parenthesis in Rust are "free" of semantics.

Personally I feel that parenthesis can make a casual reading of expressions easier and carries the intent of the author. Excessive parenthesis however becomes distracting.

I would just remove all that "#![allow(..)] stuff and let the compiler and "cargo clippy" keep me on the straight and narrow.

Also "cargo fmt" saves me from having to waste time thinking about formatting.

1 Like

Fun fact: It will (probably) actually work some time in the future.


By the way, before you’ll have to

this table gives a good overview about when you’d need parentheses in expressions.

Expression precedence

The precedence of Rust operators and expressions is ordered as follows, going from strong to weak. Binary Operators at the same precedence level are grouped in the order given by their associativity.

Operator/Expression Associativity
Paths
Method calls
Field expressions left to right
Function calls, array indexing
?
Unary - * ! & &mut
as left to right
* / % left to right
+ - left to right
<< >> left to right
& left to right
^ left to right
` `
== != < > <= >= Require parentheses
&& left to right
`
.. ..= Require parentheses
= += -= *= /= %=
&= ` = ^= <<= >>=`
return break closures

Actually quite complex though, if you ask me, so especially around symbolic operators, e.g. binary & vs. ^, I wouldn’t discourage you from writing parantheses anyways as it might increase readability after all (and usually rustc won’t warn on those anyways).

I’m referring to something like a & b ^ c vs a & (b ^ c) (both mean the same).


I do feel like there is a difference between “parantheses biting you“ and “making a bug”. Rust has a strong type system, the example you gave with the if let was just you getting a compile error for bracketing things that don’t belong together like this (as you said, best to think of if let as a single keyword). And even the examples that others gave in this thread involving tuples, in particular unit tuples (foo,) or in argument lists |(x,y| vs |x,y|, in 99% of the cases your code will still not compile with the incorrect parantheses added because you’ll get a type error.


In my experience there’s more to Rust code layout than just which parantheses to set. Using cargo fmt on your code will and respecting rust’s default warnings will make you write you more ideomatic code, which will also help you to learn reading Rust code that others have written. On the other hand when you’re unsure, just put as many parantheses as you like as there’s realistically no risk involved. If you do get warnings, you can also use tooling such as cargo fix to have it automagically remove the extra parantheses for you.

And “unnecessary” parantheses that remain that weren’t warned against usually aren’t too bad. Well, some details will always remain just a matter of taste. If you ever feel insecure whether some parenthesis was not strictly necessary and Rust isn’t already telling you about it anways (e.g. in the a & (b ^ c) example above), then you can always try what happens when you remove them, and if it does compile then double-check in the precedence table if it’s actually true that the unparanthesized version still means the same.

While I’m at it mentioning tooling that helps writing more ideomatic code, you gotta use cargo clippy, too ;-) It even contains some extra lints about parentheses like this one or even this one that actively suggests to add some not-strictly-necessary parantheses in certain cases.


By the way @quinedot

1 + (2 + 3) does not have any unnecessary parantheses, as 1 + 2 + 3 would get interpreted as (1 + 2) + 3. OTOH, the latter doesn’t trigger the warning either, so your point still stands.

4 Likes

I think this point is critical.

To add my own example, rustc doesn't warn about this either:

let h_squared = (x * x) + (y * y);

Even though the code would parse exactly the same even if the parens were removed.

It's important to know, @mcc, that rustc tries really hard not to have false positives in its warnings. (On the opposite end of the spectrum, clippy is very opinionated, and it's quite normal to allow a bunch of its lints.)

So my advice would be to take the warnings as a nice impetus to read up about whatever construct you're getting from them, and see whether you end up agreeing that that particular location didn't need them. It might convince you it's fine, you might agree to disagree, or you might find a gap where it would be better for rustc to not warn and file a bug about it.

But I'll also say that "dangerous" is actually quite unlikely. Thanks to Rust having actually a rather simple grammar (no backtracking, even) and strong static typing (both quite unlike Perl), it's unlikely that you'll find a situation in which extra parentheses will both compile and do something particularly weird.

So if you want to leave #![allow(unused_parens)] for a while, that's fine too.

1 Like

Not to derail the conversation, but I noticed this was your first post here. Welcome to users.rust-lang.org!

1 Like

I wouldn't worry too much about this. In the case of if (let ...), this won't introduce a bug in current version of Rust or in future versions.

In current versions, it can't introduce a bug because it causes an error at compile time. In future versions even if the syntax is extended to allow this, it won't introduce a bug because if (let Foo = bar) and if let Foo = bar will be equivalent.

I don't think unnecessary parentheses in Rust are particularly more dangerous (or less dangerous) than other languages with similar syntax. They are simply unnecessary.

(Accidentally creating tuples as in @OptimisticPeach's example is an interesting edge case, but there are very few places where this changes behavior and does not cause a compile-time error.)

2 Likes

I still sometimes accidentally put parentheses around my if clauses, but I always make a point to remove them immediately. I do prefer the cleaner look without them and Rust's insistence on curly braces around the conditional block makes them completely unnecessary.

It can take time and effort to adjust to a language's quirks, but I think in this case it is well worth doing. :smiley:

That little detail almost sold me on Rust immediately by itself.

I have always disliked the fact that in c-like languages there are two forms of if, for and the like. One with a block in braces and one with just a single statement and no braces. It jarred my mind as being inconsistent and was annoying and even bug prone when adding a second statement to an existing single statement conditional.

And those parens around conditionals also scratched my nerves as being redundant.

I was so happy to see the Rust folks had though about this and not just blindly followed the c-like style like so many others.

1 Like

I could envision a case where shadowing and freely accepted let expressions could introduce a bug:

let x = Some(42);
let y = 0;
if (let Some(y) = x) {
    println!("{}", y); // prints `0`, not `42`!
}

Because of this I would like to at least lint against the above. (You would likely get a lint warning about y being unused, we could extend that to actually look for this case in particular and suggest the removal of the parens).

1 Like

That doesn't match my understading of eRFC 2479, which seems to indicate that

if (let PAT = EXPR) && ...

is equivalent to:

if let PAT = EXPR && ...

However, I admit this part of the RFC is fairly vague, and it's not yet implemented in nightly so I can't test my understanding.

Parens in general do not introduce scopes in Rust, so adding/removing them does not affect scoping or shadowing of bindings. I don't think there's any plan to change this.

Ah! So if let <pat> = <expr> && <pat2> = <expr2> would be equivalent to if let (<pat>, <pat2>) = (<expr>, <expr2>)? That would make sense, wouldn't it... :thinking:

I assumed that let chains were basically equivalent to matches!, but if they aren't, then I'm wrong.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.