Remove braces from `if-else` where a single-line ternary could otherwise be employed?

I want to stress that I am not pushing for any change. I just want to (re-)open the discussion on it and also share my design insight about a block indenting design versus Rust's brace-delimited blocks.

Some prior discussion pointed to the advantage of the conciseness, less noise, and equivalent juxtaposition contrast in the absence of syntax highlighting of the ternary operator:

!prev ? cur : prev + cur

versus:

if !prev { cur } else { cur + prev }

Please read that prior discussion and the linked issue-tracker justifications for the context of my proposal below.


Note another advantage of the ternary operator over Rust's brace-delimited if-else is that Rust will allow the absence of parenthesis:

let x = if !prev { cur } else { cur + prev } + 1

Whereas, the ternary operator's precedence and lack of bracing on the tail end, requires parenthesis:

let x = (!prev ? cur : cur + prev) + 1

However, the flipside is that for programmers who don't memorize the ternary precedence and implicit right-to-left grouping, visual ambiguities can result in programmer error:

let x = 1 + !prev ? cur : cur + prev + 1


So I contemplated that assuming syntax highlighting is always present, then the optimum for Rust (and even for a language that employs block indenting instead of brace-delimited blocks) would be to drop the braces and employ parenthesis for the single-line ternary case:

if (!prev) cur else cur + prev

Unfortunately, without syntax highlighting the contrast of else is lost in a thicket of words:

if (!prev) cur else cur + prev

I don't think we should dismiss the case of code being read in a "view source" or text editor scenario where syntax highlighting is not present. Designing a language to be independent of tools is an important goal as well.

One possible solution is to require the keywords to be uppercase (at least in this single-line scenario but consistency of case of keywords is important), but I think it to be noisy:

IF (!prev) cur ELSE cur + prev

Especially in the non-single-line case (shown below with block indenting instead of braces):

IF !prev
   cur
ELSE
   cur + prev

And redundant when we have syntax highlighting present:

IF (!prev) cur ELSE cur + prev

An alternative for the single-line case, is to require : instead of else:

if (!prev) cur : cur + prev

But if we are going to butcher if-else for the single-line case, why not just require the ternary operator in that case:

!prev ? cur : cur + prev

The arguments against ternary are:

  1. Strong reason needed to revert the prior decision to remove it.
  2. Consistency.
  3. Explicit semantics of if-else versus symbol soup.
  4. Parser grammar conflicts or just visual confusion with other uses of : in expressions, e.g. coercion to a type x : Type.
  5. Nested ternary is difficult to read especially without nested parenthesis.
  6. Visual (not grammar) ambiguities due to precedence and right-to-left grouping.

My responses:

  1. For Rust since it has the brace-delimiting for contrast, the only reason is to make it less noisy and more concise. This may not be a strong enough justification for Rust.

  2. Consistency is important, and for Rust the brace-delimited syntax works in both single-line and multi-line cases, albeit with slightly more noise, verbosity, and wasted vertical screen real estate:

    if !prev { cur } else { cur + prev }

    if !prev {
       cur
    }
    else {
       cur + prev
    }
    

    But the block indenting language case:

    if !prev
       cur
    else
       cur + prev
    

    Can't maintain contrast in the single-line case without syntax highlighting:

    if (!prev) cur else cur + prev

    if !prev : cur else cur + prev

  3. Rust's brace-delimiting maintains the explicitness of if-else without losing contrast in the absence of syntax highlighting albeit with more noise and verbosity of braces. A block indenting language has an less noisy and concision advantage in the multi-line case, but can't maintain consistency in single-line case unless force more noise in the multi-line case:

    if !prev :
       cur
    else
       cur + prev
    

    Or:

    if (!prev)
       cur
    else
       cur + prev
    

    With the latter being superior in the absence of syntax highlighting:

    if (prev)

    As compared to:

    if prev

    But in the single-line case this loses contrast around else in the absence of syntax highlighting:

    if (!prev) cur else cur + prev

    Albeit just replacing the else with : is more consistent than a ternary:

    if (!prev) cur : cur + prev

  4. Assuming there are no conflicts in the grammar, it is possible for the compiler to generate an error to force the programmer to nest such visually confusing cases in parenthesis. Note my proposal to require type names to begin with uppercase and variable names to not, IMO resolves the visual ambiguity for the case of coercion prev ? cur : Type : prev versus prev ? cur : prev without parenthesis although I'd prefer to see that written prev ? cur:Type : prev. Even in a language (e.g. Scala) which enables constructor without prepending with new, afaics there isn't ambiguity in the grammar nor visually between prev ? cur:Type : prev and prev ? cur : Type(prev).

  5. It is possible for the compiler to disallow nested ternary operators which aren't grouped by parenthesis.

  6. When the expression to the left of ternary's first operand expression includes an operator which could be visually ambiguous due to the absence of parenthetical grouping, the compiler can require that either the first operand of the ternary or the entire ternary be enclosed in parenthesis:

    let x = (1 + !prev) ? cur : cur + prev + 1

    Or:

    let x = 1 + (!prev ? cur : cur + prev + 1)

    Or in a block indenting language, instead only replace the else with a colon:

    let x = if (1 + !prev) cur : cur + prev + 1

    Or:

    let x = 1 + if (!prev) cur : cur + prev + 1

Thus I have concluded that for Rust, the best decision is no change, i.e. no ternary operator. For a block indenting language, it appears the optimal design is if-: for single-line if-else only when it is contained in an expression that is assigned:

let x = if (!prev) cur : cur + prev

foo.method(if (!prev) cur : cur + prev)

And note if expression is also an assignment (aka operand) to an implicit if function:

if (if (!prev) cur : cur + prev) x = cur

So instead of where the if expression is not assigned, thus is only a statement:

if (!prev) x = cur : y = cur + prev

Programmer must write:

if (!prev) x = cur
else y = cur

Or:

if (!prev)
   x = cur
else
   y = cur
1 Like

Generally the style without braces is considered bad as it can lead to hard to spot errors where a statement is added to the code and the programmer thinks it is conditional, but it is in fact not. For ages C/C++ style guides have recommended to never use the baceless form of block statements like if/while/do in C/C++. Rusts approach is to make what is considered good style in C/C++ mandatory, because experience, and many code reviews tells us it results in less errors. It may not look as pretty, and it may require more typing, but both of those issues are less important than correct code.

I know. But I am not recommending braceless blocks unless the language is enforcing block indented delimiting, which afaik then removes the ambiguity both from the grammar and visually, thus entirely eliminating the problem.

Thanks for making the point so that other readers aren't thinking I am incorrect, because perhaps I didn't state explicitly enough that C/C++ style optional block indenting with optional braces, is not the same grammar as enforced (i.e. non-optional) block indented delimiting.

With enforced block indented delimiting, then following are never confused:

if (cond)
   a
b
if (cond)
   a
   b

And the dangling else is unambiguous because the following two are not equivalent:

if (cond)
   if (cond2) a
   else b
if (cond)
   if (cond2) a
else b

P.S. another advantage of enforced block indented delimiting as opposed to brace-delimiting, is that we remove the style arguments about where to place the braces:

if cond
{
   a
}
else
{
   b
}
if cond {
   a
}
else {
   b
}
if cond {
   a
} else {
   b
}

There are other problems, it makes cut and paste difficult (as you have to re-indent, and get that correct or you change the meaning of the code) and it prevents people using formatting tools (like the on the Rust playground) to quickly make code look good, or conform to house style guides. Tabs vs spaces also cause issues, especially if you use non-stardard tab-widths.

I agree the tab versus spaces style issue creates acrimony. Afair Python detects the usage dynamically. I would have opted for 2 spaces per indent to resolve the issue with the least corner cases (and those who don't like it can create their own fork ;-P) and minimum consumption of horizontal screen real estate. Those who want greater horizontal indenting can configure an IDE to render how they prefer.

With a smart IDE, there are no additional issues with cut-and-paste and pretty formatting. Afaics, the Rust playground would not have issues.

The text editor can simulate tabs with spaces.

Death to brace-delimited blocks and tabs! :imp:

P.S. in the past I contemplated that enforced block indenting violated the End-to-End principle, because different ends would render differently, but I think this is due to undefined tab width. Now that I think more about it, tabs aren't an End-to-end principled violation yet rather an ambiguous format.

Even with brace-delimiting, aren't we all sick of cut-and-paste code from different tab width than ours and the resultant columnar format is unaligned. Let's just force consistency.

At least ugly cut an paste code doesn't change meaning due to different indents. Most of the advocates for significant whitespace rely on the "good IDE" argument. As someone who programs using Vi, that's not going to be much use to me. I have no use for code completion either, I generally keep a web browser open on the standard library docs, and that's it. There is no point in bring able to type code faster than you can think it :slight_smile:

I don't have a problem with languages like Haskell (where significant whitespace is optional) nor Python where it's mandatory, but I don't have a problem with braces either. I do however think it's best to stick to one or the other. Doing it partially seem the worst of both options.

1 Like

When one consistent indenting style is enforced by the compiler, then cut-and-paste can't change the block delimiting within the code pasted, although how much the pasted code is indented will determine what block indent level it is on within the code it is being pasted into. But this is quite obvious to the eye. Simple highlight it and press the Tab key or shift-Tab key to indent or outdent as you desire.

I don't think I advocated enforcing it partially. I also didn't advocate significant whitespace; instead I am proposing to decrease the whitespace Rust consumes. But I am not arguing for changing Rust. I am writing about what is possible in language design.

This is the kind of problem you get:

if a then
   b = a;
   if b then
      ...

Means something different to

if a then
   a = b
if b then
      ...

Why is that a problem? Habit? Looks obvious to me. I classify that as a desirable feature:

If you leave out a brace it's an error, so to change the meaning requires two changes to remove a brace at one place and insert another somewhere else. Removing a single brace will always get caught. It's like the parity check on ECC RAM. With the indent method there us no safety check.

This looks dangerously close to goto fail with an additional indentation twist.

I'm afraid I don't understand. Please don't attribute a block formatting to me that I did not write.

The formatting you have displayed would generate a compiler error.

There is no parity check when you place your braces not as intended. There is no parity check when you indent not as intended. Both are visual in front of your eyes. If you put your cursor one line too low when you paste with braces (so the end } is above your paste location), or if you paste and don't get the indenting where you intended even though it is right in front of your face, then in both cases you get an unintended result. I don't see any increased fail safety for braces.

And if programmers are so inept they ignore what they see in front of their faces and we use that as an excuse to clutter our code with noisy braces, then we have a serious problem with software quality because there are much more subtle cases where what appears to be correct visually is not due to precedence, operating grouping, implicit coercions, etc. The indenting is quite obvious and there are not ambiguities that cause the grammar to differ from what is visual.

Sorry, that seems to be a discourse rendering bug. :frowning:

The conditional in question is:

if (cond)
   a
b

if (cond)
   a
   b

skade's example is:

if (cond)
   a
b

if (cond)
   a
   b

Assuming b is not a statement, i.e. is only an expression with no side-effects{1}, then the first case should be a compiler error unless it is the last line in the function and matches the result type of the function. Otherwise we must assume both are intended by the programmer.

But how is this any more human error fail safe?

if cond {
   a }
   b

Or this:

if cond {
   a
}  b

Aren't we pushing beyond the realm of reasonable noise added for no clear gain?

It seems you two are arguing that the programmer needs more noise to keep him focused on where he intended for his block to end. There is nothing ambiguous in any example above. The programmer's intent is visually displayed. Why can't he see what he intended ???

Edit: {1} Differentiate this from the distinction between a rvalue and lvalue.

1 Like

Different meanings based on indentation is going to cause problems with formatting tools. I have rustfmt setup to run on save in my editor and when I forget a closing brace or parentheses it re-indents the whole file. Adding the missing brace and running rustfmt again solves the indentation. With indentation sensitive code this would probably cause a disaster..

A formatter can't auto-indent a block indenting delimited syntax. So no disaster can happen. You have to do your own indenting when you type. You can use editor features to aid you, such as have it automatically indent when you press the Enter key, and the ability to select a block and change its indenting.

The compiler will enforce that your indenting has to be consistent, in the sense that it won't allow you indent more than one level, unless it is the legal continuation of the prior line and in which case it is not a block. And the compiler will always enforce that your outdents are aligned on a valid block indent.

I've read that code readability is the most important feature of a programming language, especially these days in open source. IMO brace noise, removes focus from actual code. Python is raved to be one of the most readable languages by Eric Raymond who coined the term "open source" with his famous The Cathedral and The Bazaar. It is also documented by survey to take only 3 - 6 months to learn versus 1 - 2 years for C++.

So if the programmer spends just a wee bit more time indenting than use an indenting formatter (a tool I've never used), not only will the code be ostensibly more readable and consume less screen real estate, it will also be consistent versus Rust programmers may decide to use different formatters. Now I understand I could run my own Rust formatter, but I might be viewing the Rust brace-delimited code on a website that doesn't run my choice of formatter. I argue for consistency and concise readability as a high priority, given all other factors of a design decision are nearly equal.

Again I am not arguing for a change in Rust. I am just arguing in terms of language design in general.

It might just be a matter for personal preference. Those who prefer very noise-free code, are probably going to favor my design sense. Hardcore C/C++ programmers are probably not going to like it.

P.S. I used to argue against Python's block indenting. Since learning to design parser grammars and becoming more intimate with ambiguities, my analysis changed.

Other non-enforced (compiler) formatting style could still be performed by a prettifier such as the Rust playground.