Dropping Curly Braces From Single Statement `guard ... else` Blocks

The vast majority of the guard statements I write contain a single statement in its else block:

  • return
  • return value
  • throw ErrorType()
  • continue
  • break loopLabel
  • etc…

This is fine and good, and I would never suggest we rename or shorten these phrases as their intent is particularly clear, but years later, I still end up not typing the curly braces on first pass, only remembering to add them after the fact. Since I don't think this particular iteration of a guard shorthand has been pitched yet, how do people feel about optionally dropping the curly braces when only a single statement lives in the else block?

Some before and afters:

Simple Unwrapping, Return nil

guard let unwrappedValue else { return nil }
guard let unwrappedValue else return nil

Multiple Checks and Throw Error

guard currentLoad > watermark, let excessLoadManager else {
    throw LoadError.missingExcessManager
}
guard currentLoad > watermark, let excessLoadManager
else throw LoadError.missingExcessManager

Loops

for job in sortedJobs {
    guard job.isValid else { break }
    job.start()
}
for job in sortedJobs {
    guard job.isValid else break
    job.start()
}

Multi-statement else blocks would continue to require curly brackets as is the case today:

var firstInvalidJob: Job?
for job in sortedJobs {
    guard job.isValid else {
        firstInvalidJob = job
        break
    }
    job.start()
}

Although I understand why we don't allow single statements without braces for if-statements, loops, and functions, and would never argue to remove braces there, I think the unique spelling of guard ... else statements actually makes things more readable when the else phrase can flow directly into the single statement without interruption, much in the same way dropping parentheses from if conditionals helps readability there.

I don't believe introducing this new sugar would break any existing code by introducing ambiguity, nor would using it cause any parsing issues, but I could very much be wrong here. All this said, this is my first time submitting a pitch to the language, so I would appreciate any help I could get if there is any shared interest in making this happen.

8 Likes

I love this idea. I've been toying with a macro which would turn a typical guard statement in something like

#guardlet myVariable

But I don't love the readability of it.

1 Like

I suspect that whatever the rule ends up being, should probably apply to all of if/do/while/repeat, not sure whether the else scope of a guard statement is fundamentally different to the other ones to justify special treatment.

1 Like

The forcing of {} around flow control is something Swift gets right imho.

Adding a statement after an if, not realizing it's not in a {} or refactoring and messing up in similar style is a real source of security and correctness bugs. I suffer every time I write C++ lacking {} and it constantly worries me that probably some of them are wrong -- especially if directly followed by another line, without empty space after the one statement "inside" the if.

The specific case of guard else return is something that makes more sense to allow since it is truly "nothing past this single line will execute, and one cannot accidentally mean "oh and something after the return too".

48 Likes

I don't think we'll want this for if and while loops as they both take a condition, while else does not: if let returnValue anotherValue += 1 becomes impossible to parse unambiguously. Even if limited to return statements like guard requires: if let returnValue return returnValue could become a lot harder to read over depending on syntax highlighting, not to mention the easy accidents that could occur once this is split to two lines as @ktoso mentioned above.

do statements tend to be used to catch errors, so do return catch ... or do throw MyError() catch ... make little sense, and do return nil makes equally little sense if do is used to group statements.

repeat I can maybe see the utility of, but I have never reached for dropping curly braces there, so a good example would be needed.

I therefore suggest we keep the scope of this pitch to guard ... else blocks, though follow up pitches could certainly expand on it as needed.

1 Like

I believe this is truly a nightmare that

func foo() {
  guard codition else return
  if condition2 {
    print(42)
  } else {
    print(21)
  }
}

will execute if as an expression.

25 Likes

That is rather unfortunate. I wonder if the compiler could prevent such a situation by requiring the return to be on the same line as the statement it's returning when this shorthand is being used, though that may prove to be too much special casing for some readability sugar. What do you think?

ie.

func foo(condition: Bool, condition2: Bool) {
  guard condition
  else return if condition2 { // Executed only when condition is false
    print(42)
  } else {
    print(21)
  }
}

would execute the if condition, but

func foo(condition: Bool, condition2: Bool) {
  guard condition else return
  if condition2 { // Executed only when condition is true
    print(42)
  } else {
    print(21)
  }
}

will not.

Moving this to "Discussion" as that would be the first step in the Swift Evolution process.


Overall, this is a well trodden area and I'd urge you to review past discussions regarding syntactic sugar for guard blocks.

As @ktoso has said, Swift deliberately breaks with C/C++ in requiring braces around single statements after if. I would argue that the overall direction of the language—see recent discussion about how to expand if and switch expressions to allow multiple lines—is to smooth out (rather than introduce) "cliffs" where writing a simple thing is easy but adding another simple thing introduces combinatorial friction.

To me, a key property of "progressive disclosure," properly implemented, is that essential complexity is introduced in a graded manner and at the appropriate point. Since { } delimits control flow, it would be appropriate to introduce it when control flow is introduced, rather than hide it at first while deferring its sudden appearance until an arbitrary later point (such as when something is written in two lines instead of one). I tend to find that arbitrariness tends to compound on itself, as perhaps you're finding as you contemplate special line breaking rules around return.

14 Likes

I concur with @ktoso . The ability to omit curly braces around control flow is the root cause of the goto fail bug.

Leaving off curly braces looks nice in the short term, but it re-introduces entire classes of problems that Swift was designed from the very beginning to eliminate.

Example 1, "Safety" section

Example 2 from @rickroe

21 Likes

Yep, I largely agree with the sentiment that control flow without braces has the potential to be very dangerous which is why I explicitly mentioned that such an improvement would be limited to guard ... else statements.

With an additional restriction that the braces can only be omitted if the else keyword, return keyword, and start of statement must be located on the same line, I think we largely avoid most of these foils.

Namely, the following examples are all allowed:

guard condition else return
guard condition else return value
guard condition 
else return
guard condition 
else return value
guard 
    let unwrapped1,
    let unwrapped2
else throw ErrorType()
guard 
    let unwrapped1,
    let unwrapped2
else {
    self.error = ErrorType()
    return
}
guard 
    let unwrapped1,
    let unwrapped2
else 
{
    self.error = ErrorType()
    return
}
guard condition
else 
{
    return
}

While the following could generate some diagnostic:

guard condition else
return /// <-- Error: Multi-line else clause must be wrapped in curly braces
guard
    let unwrapped1,
    let unwrapped2
else throw
MyError() /// <-- Error: Multi-line else clause must be wrapped in curly braces
guard condition
else return /// <-- Error: Non-void function should return a value
value /// <-- Warning: Expression of type 'Type' is unused

Since the whole expression must occur with the else keyword, I think that both a) would match formatting best practices for legibility and b) would make accidental edits hard to make, as the compiler would not allow most cases where inserting or re-ordering the line would cause issues, especially since this line would be forced to have else somewhere before its expression.

2 Likes

Out of necessity (largely to introduce new syntactic constructs with context-sensitive keywords that need to be disambiguated from their possible use as identifiers), Swift has a non-zero but small number of curious newline-is-significant parsing rules (too many for my taste, IMO). For example, the expressions consume x, copy x, and each x cannot be broken by a newline. However, this is out of a need to avoid ambiguity when implementing entirely new sets of important language features.

Introducing newline sensitivity around existing non-contextual keywords like else, return, and throw—and only in some but not all uses of them!—seems like a recipe for confusion, for writers, future readers, and tools alike. When weighed against the alleged benefit that it allows two curly braces to be elided, it's very hard to rationalize introducing that complexity into the language. The braces simply don't seem that egregious.

18 Likes

Personally I'm okay with newlines being significant, because that matches how it works in human languages. But, I've seen some [to me] bizarre styling in common use throughout the Swift community, that clearly rejects that premise. Thus this proposal unavoidably butts up against people's "stylistic" preferences - where to put the curly braces etc - which is the most toxic kind of code debate. I, for one, wouldn't go there.

3 Likes

To be fair, the everything on one line restriction would only apply to the single statement case — as shown in my examples, if you want to continue to use curly braces, you would be able to place those wherever you stylistically prefer.

As a final point of clarification here, it isn't really about the "saved characters" here for me at all. I often times write out an entire statement, then go back and editorialize it anyways, so any time saved not typing { and } would have been lost on me. Instead, I do see these braces as distracting from what the guard is trying to do when reading through the code. As guard statements require some sort of control transfer statement, and are often just a single control transfer statement, they feel different enough to warrant some improved sugar.

Seen another way, I think reading guard ... else return enforces the idea a bit better in my opinion that we are returning out of the context the guard lives in, rather than jumping from the else-clause to the guard, and finally to the calling function or closure:

guard let context else throw ManagerError.missingContext

As a side benefit, it becomes much more obvious when reading through code that a guard is doing a bit more in it's else clause if it uses braces, as those will naturally draw the reader's attention over them not existing:

guard let context else {
    attemptAsynchronousRecovery()
    throw ManagerError.missingContext
}
1 Like

Had a great chat with @dimi offline that I wanted to quickly summarize here:

I'm leaning -1 for this because consistency is king for me. The (potential) inconsistencies in reading some code with {} and some without isn't something I think I would want to deal with. A huge portion of my job is reading code (review, resolving bugs, onboarding with clients etc..).

I, personally, prefer the {}s because I like a certain level of explicitness and I'm always skeptical of "sugar" because les typing != better code. But even if I didn't a "style" of code I can immediately run through my brain parser and understand trumps something I need to pause on (regardless of how I personally feel about it).

Others have made good points above re: compiler complexity, identifying scope etc. so I won't rehash those.. I just wanted to add a comment from a human consumption perspective.

5 Likes

I don't have new insights on the new-line-before-returned-value issue, but I've always thought that any potential brace-less version of guard syntax should be strictly limited to a single control flow statement (return, break, continue, throw, …).

However, although it took a couple of years, I did get over caring about "unnecessary" braces after the else.

I'd actually suggest that the problem could be largely solved by adding a warning with a suggested fixit in that scenario: else followed by a control flow statement.

Better to streamline getting to the desired syntax than changing what syntax is desired, perhaps?

2 Likes

You're overlooking compound statements. For example:

guard x > y else if x == 0 { return foo(x) }
else { return bar(x) }

If you can directly append return after else, then you should be able to do that with if, for, another guard etc.

There may even be a dangling-else issue:

guard predicate0 else guard predicate1 else // erm....

I love this kind of thing in languages that support it:

for (let row of rows) for (let column of columns) yield Cell(row, column);

Swift is not the place for this though.

1 Like

-1. While it's a nice idea for optimization of some lines, it opens a can of worms with regards to general parsing / semantics / unwanted bugs.

5 Likes

IMHO proposal only looking good with one liners. For Multiple Checks and Throw Error part {} is much more readable.

Imagine the docs...

Semi-colons are generally optional, but blocks always require curly braces, except else-clauses that are associated with guard-statements and only contain a single statement (that is not a compound statement), in which case, they're optional.

These kinds of caveats are painful for new users, who are not yet able to appreciate why that specific case would be special.

4 Likes