A look at pros and cons of brackets on if statements

One of the features that Rust has that makes it unique is the lack of necessity of parentheses on if statements! Good, those are annoying!

The reasoning for the necessity of parentheses in C++, comparatively, is a separator; brackets are not required, so without the parentheses, the statement would slosh together. Omitting the brackets is allowed because, for the same reason, they're annoying.

Rust decided that it would remove the requirement for parentheses in favor of requiring brackets. This was a good idea because it's a good trade-off to focus on a single one of the two separators. Additionally, it is very common to make bugs in C++ where the distinction of a single line after an if statement is missed, so making brackets would make it a lot easier.

And the story ended here.

However, people still have to press shift+[ and shift+] on every small if statement, and it feels a bit old for Rust, which has more potential. Although the IDE can help move around your brackets, not all do so, nor 100% fix the problem, which doesn't need to be there in the first place. Here are two ways, the first less large than the second, that bracket rules could be improved.

  1. Allow limited keywords without brackets. "continue" "break" and "return" are very, very commonly used keywords after if and else statements, for more complex control of loops. The brackets are very annoying to navigate around, and do really clutter up the code. An exception of permitting dropping the brackets for single uses of these keywords would go a long way, and would be surprisingly seamless -- the lack of brackets makes it clear that the block doesn't continue, and the keywords wouldn't interfere with any expression logic at all.

  2. A separate breaker term. Because brackets are inconvenient, hard to type, and reduce navigability by being on both sides of a script, instead of removing their requirement, a different term could be used after if or else statements. For example, then or do (do specifically because it is already a reserved keyword). This would allow for single-line conditionals and would also be effective at removing confusing distinction errors because of the necessity for another word in between, although it would feel a lot easier to type. Additionally, rule 1 could be coupled with this. It would be possible that an else statement could not require a separator at all because it doesn't need it, but that would probably not be a good feature because it would be confusing that the else statement would have different rules than the if, and because it wouldn't then benefit from the bug-reduction factor.

This change would almost certainly have to be released in a different Edition, and be thought-out, because it is large, but one of these ideas should be done because it would really make a positive difference to the language. Rust has a focus on feeling smooth to use, but these brackets add a lot of friction.

1 Like

In my view, half of the friction comes from rustfmt tending to break up if expressions into multiple lines very eagerly. You call the goal out yourself as "allow for single-line conditionals", even though Rust is not a whitespace sensitive language so anything can become single-line without a problem already, and braces, compared to a keyword, even contribute in keeping the line shorter.

Though I do feel there is some appeal in considering a special case for return, break and continue.

23 Likes

Major thumbs down.

Rust's uniformity here is a good design. It reduces cognitive load when reading the language.

No one cares how many keystrokes you have to type - code is read many orders of magnitude more than written and optimising writabilty in expense of uniformity and readability is bad design.

I also don't get the point about ide support at all. This suspiciously sounds to me like a complaint about keystrokes & navigation in vi.

Edit: if there are cases where rustfmt doesn't lay out the code optimally than yeah that could be improved upon.

13 Likes

Makes sense, although from my experience the brackets are a lot of the problem when reading it. When there's so many and you just want to add one guard clause, it can make it look so much more complicated than it needs to.

And that's why I brought up #1 as the smaller change. I understand that each feature is at a cost of a different one. It's just a matter of optimizing the language for everybody. I don't necessarily think we should go as far as #2.

Well, keystrokes are important. A LOT of time is spent by programmers messing with the brackets, like adding them on both sides or pressing end enter to get to the next line out of them. Change #1 would probably save at least 0.1% of time spent writing Rust code.

1 Like

Historically it went the opposite way. It was first decided that brackets had to be mandatory because skipping them can lead to mistakes like the now famous "goto fail", and because it was making the language simpler with only one way to handle condition. The decision to remove the parenthesizes came later when it came up they were no longer necessary. Requiring the brackets was a conscious decision, and in my opinion a good one, that's the reason I doubt it will change.

To me, being hard to type is a weak argument for syntax decision in a programming language. And in this case it is even more weak because requiring only a shift key press is a very tiny issue. Even on my French keyboard where I have to use the Alt gr key, it's bearable. Brackets are not an uncommon character in Rust anyway, it is used extensively everywhere in the language, the Rust programmer have to be used to type them.

Legibility and mistake avoidance come far before typing ease in my priority list. What you are proposing would make the language more complex with more specific case to know.

16 Likes

Person with RSI here. At one point I used AutoHotkey to make curly brackets a single keystroke. Many other technological conveniences are also available. Changing the language is not the right solution.

11 Likes

Very strongly opposed.

This request simultaneously admits to being a big change but spends almost zero time on motivating why Rust is deserving of such a change. It seems to presuppose that removing brackets is a goal everyone should share. And even if it was, it also presupposes that we should do it at great expense.

17 Likes

Heh, I wish it were as ergonomic on all layouts as the US layout’s shift plus the keys to the right of P. For example, on Nordic (and eg. German) keyboards which have to accommodate three extra letters, {} are typed with altgr+7 and altgr+0, and on Mac even more awkwardly with alt+shift+8 and alt+shift+9! Looks like the French layout is even worse…

6 Likes

In 2014 I gave a talk on Rust cryptography and one of the things it highlighted was the recent (at-the-time) "goto fail" vulnerability which would've been prevented by mandatory braces.

Mandatory braces are a useful tool for preventing bugs, and one I'm very glad that Rust has them.

6 Likes

Agreed on this. There must be some separator, and forcing the {}s instead of ()s was a great change from the C/Java/etc usual.

But because there must be a separator, I don't think it's worth trying to get rid of the {}s too.

Another choice isn't worth it at the syntax level.

3 Likes

Specifically, forcing the {} for the body of the if, as opposed to () for the condition, helps avoid human error.

I have seen far too many bugs of the form:

if (feature_gate("my_feature") && do_the_thing)
    my_feature_init();
    // Next line added later, intended to be part of the `if` body
    use_my_feature_for(user_id);

where the intent is clear from the layout, but the result is a runtime error because you've called use_my_feature_for without first initializing the feature.

Making the brackets required for the code block prevents this sort of refactoring error. In Rust, the original would have been:

if feature_gate("my_feature") && do_the_thing {
    my_feature_init();
}

and thus when I add use_my_feature_for, it'll always go in the right place.

6 Likes

NOT A CONTRIBUTION

Note that this is a self-contradictory goal, since optimization always makes assumptions in order to exploit them, which reduces generality.

2 Likes

With diverging control flow (break, continue, return) AFAIK there's no risk of a "goto fail" error. To me, only allowing these is the most acceptable option. However, I still have a couple of concerns:

  • "always add {}" is a simple rule, and "{} is optional for these keywords but not those keywords" is messier. There will be questions like "why not allow else loop {}" too, etc.

  • when () is required, it visually separates the condition from the action: if (cond) return;. Otherwise it's just a string of words one after another:

    fn foo() {
       if cond return bar;
    }
    

    Syntax highlighting helps here, but overall it's less compelling. Especially if the condition was longer. This could result in a bikeshed about whether to use () for condition in this case or not, and bikeshed whether to use this form or stick to { return; }.

Keyboard layouts without easy access to {} are very unfortunate, but one change of one syntax case won't make a significant difference — there's still plenty of brackets in Rust all over the place.

Perhaps the layouts should be changed? The Polish language has migrated from a traditional "Polish typewriter" layout to "Polish programmer's" layout. It's literally called like that, and it's now the default on all computers.

6 Likes

Don't forget that the condition can be a block:

    if { let foo = 1; foo == 1} { println!("Yes") }

is legal, warning-free Rust. Rust does warn you if you use a block unnecessarily:

    if {foo == 1} { println!("Yes") }

generates a warning for the braces around the condition.

Whether this helps or hinders clarity is in the eyes of the reader - but I'm not convinced that:

    if { foo == 1 } return;

is any clearer than

    if foo == 1 { return; }
3 Likes

If a shortand for this were required, I would prefer the Ruby syntax:

fn foo() {
   return bar if cond;
}

Which can't be confused with the current syntax and naturally allows a {}-free experience.

1 Like

I agree, but I think it's still a poor fit for Rust because it's also very common for the diverging thing to be a panic! (or could just be a -> ! method call) which doesn't have that nice syntactic separation.

Given that even let-else doesn't allow else return;, I don't think it should be special-cased for if.

2 Likes

Syntax highlighting also works even better with braces. For example, innermost braces may be bolded. Plugins like Rainbow Brackets also highlight brackets at different nesting levels with different colours, which both makes them immediately visible and easy to track expression nesting.

Those are extremely debatable statements. On standard US layout, braces are only marginally more inconvenient than brackets ([ ]), requiring an extra shift press. They are significantly shorter to type than delimiter keywords: it's a single chord vs several letters in a specific syntax. This looks like an objection from a Vim fan, who already maps as much as possible to letter keys and doesn't want to move their fingers even a bit. Maybe it does indeed have benefits for a trained typist, but most people aren't one, so I don't believe that typing { } is on average any more complex, or results in more typing errors.

There are also many typing assistance tools which can be used to fine-tune { } typing. You can use sticky keys mode, which would allow to type shift + [ as a sequence of keypresses, rather than a simultaneous chord. You can remap { } to different keys, or different chords. Editors can (in principle, I don't think any currently do) automatically insert braces after if condition, or automatically change typed [ in that position into a {. In fact, the latter looks like a nice addition for IDEs.

"Reduce navigability" is also very odd. Braces significantly improve code navigation during typing, because you can easily jump between innermost or matching braces with a hotkey in most quality editors. The same could work, in principle, with keyword delimiters, but then there would be the question of "which part of a multi-character keyword should such actions jump to?" Certainly whichever part you choose, occasionally it will be the wrong one, so now one would also need to navigate over a keyword. How's that an improvement?

Other issues with keyword delimiters, in no specific order:

  • There are good reasons why most popular languages have converged on brace-based or indentation-based syntax. Nobody wants to type keywords where a single character would do. Nobody wants to remember different keywords for different syntactic constructs (loops, conditionals, functions etc), when the construct's syntax is already succinctly expressing that information, and you only need to express logical nesting.

  • Alternative syntax is a very poor proposition in general, even if keyword delimiters could be accepted into a language built from scratch. The benefits are slim. The downsides are numerous: varying styles in the same codebase are even harder to read, so now everyone must somehow decide on a single style and enforce it. Keywords are also syntactically too similar to identifiers. This can cause parsing complications, as well as worsen readability, and reduce IDE helpfulness (because it relies on unambiguous pieces of syntax to guess the possible completions and fixes for syntactically incomplete constructs).

  • A different block syntax would be a significant complication for macros, which would need to double their parsing efforts for no real benefit. It would also increase the possibility that the alternative syntax isn't handled properly, and can conflict with currently allowed token sequences (causing a misparse in existing macros). Macros are also a case which is hard to impossible to fix automatically with cargo fix.

I don't think so. There is still a possibility that someone just forgot to finish their if-statement, so that the subsequent break becomes part of the condition. I.e. this:

if cond
break;

turns into

if cond { break; }

causing an error. Yes, it's a bit of a weird mistake, but so is "goto fail". Programmer getting distracted and forgetting to finish some piece of code is a realistic scenario, and Rust thus far has very strong protections against it. Most of such errors won't compile, I see no reason to add an exception.

This gets even weirder once you consider interaction with other construct, e.g. let-else.

let Some(foo) = if cond { bar } else break 'search else return true;

At minimum I don't find it pleasant to read.

4 Likes

The form where the mandatory braces annoy me the most is let-else:

let Some(x) = foo() else { return };

The else-block must diverge, and often would be a single control flow expression. Because this construct is a let-statement, the semicolon after the closing brace is also required, making the braces feel redundant.

That currently has to be written as:

let Some(foo) = (if cond { bar } else { break 'search }) else { return true };

Specifically, the matched expression cannot end with a brace so it needs parentheses. (This surprised me, but it seems very reasonable.)

I would preferably allow let-else to take a diverging expression for the else (and no changes for the other conditionals). I don't think your example would be any less clear with that:

let Some(foo) = (if cond { bar } else { break 'search }) else return true;

If anything, this looks better: You can see at a glance that the trailing else return doesn't have any kind of braces, so the tangle of them around the middle of the line is probably one unit.

3 Likes

goto fail; is a diverging statement... As I understand it the issue was that the goto fail got accidentally reduplicated and the second one unexpectedly applied outside the if statement.

3 Likes

The duplication looks likely to have been a bad automerge by a source control tool.

The exact code at fault was:

    if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
        goto fail;
    if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
        goto fail;
        goto fail;
    if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
        goto fail;

Keeping the C-ness, but using Rust's rules for parentheses and brackets (and keeping the bug) would have made this into:

    if (err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0 {
        goto fail;
    }
    if (err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0 {
        goto fail;
    }
        goto fail;
    if (err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0 {
        goto fail;
    }

With this indentation (one of two likely outcome from a bad automerge), it's obvious what the bug is when you review the code. Even if you ran a formatting tool, the unconditional goto fail before an if would be clearly wrong during code review. If the other outcome (goto fail; twice inside the block) was the automerge failure, then there wouldn't even have been a bug.

And, of course, if you have an error on dead code, this would also be caught - everything after the second goto fail is dead code. But messy C codebases often don't turn on that error, because it fires too frequently to be useful :frowning:

1 Like