[proposal] Allow trailing closures in 'guard' conditions


(Chris Lattner) #1

Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I consider it to be a bug fix, but since it is a language extension, I feel that it should go through the evolution process. Thoughts appreciated!

-Chris

Introduction & Motivation

The three statements `if`, `while`, and `guard` form a family that all take a rich form of conditions that can include one or more boolean conditions, `#available` clauses, and `let`/`case` pattern bindings. These are described by the `condition-clause` production in the TSPL reference section and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions embedded in the condition, because that would be generally ambiguous with the body of an if or while statement:

  if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by performing arbitrary lookahead or by performing type checking while parsing, these approaches have significant consequences for the architecture for the compiler. As such, we’ve opted keep the parser simple and disallow this. Unrelated to this proposal, I landed a patch (https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446) which *greatly* improves the error messages in some of the most common cases where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements, it does not make sense for ‘guard': The body of a guard statement is delineated by the `else` keyword, so there is no ambiguity. A brace is always the start of a trailing closure.

From a historical perspective, the current situation was an oversight. An earlier design for `guard` did not include the `else` keyword (it used the `unless` keyword), and I forgot to fix this when we decided to resyntax it to `guard/else`.

Proposed solution

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

Detailed Design

The impact on the compiler is trivial, here’s a patch:

Impact on existing code

There is no impact on existing code. This only makes formerly invalid code start being accepted.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

Do nothing: It can be argued that this change would make guard inconsistent with the restrictions of ‘if’ and ‘while’ and that inconsistency would be confusing. On the other hand, I am arguing that this is an arbitrary restriction.

Expand the scope of “if” and “while” statements: Through enough heroics and lookahead we could consider relaxing the trailing closure requirements on `if` and `while` statements as well. While this could be interesting, it raises several ambiguity questions. For example, we need significant lookahead to realize that “a” here is not a trailing closure, since we have a closure expression being fully applied after it:

  if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but since any such direction would be compatible with this proposal, I see it as a separable potential extension on top of this basic proposal.

Change the syntax of guard: I only list this for completeness, but we could eliminate the `else` keyword, making guard more similar to `if` and `while`. I personally think that this is a really bad idea though: the guard statement is not a general `unless` statement, and its current syntax was very very carefully evaluated, iterated on, discussed, and re-evaluated in the Swift 2 timeframe. I feel that it has stood the test of time well since then.

guard.patch (3.23 KB)


(Brent Royal-Gordon) #2

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

This sounds perfectly reasonable.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

  if expr then { code }
  while expr do { code }
  for elem in expr do { code }
  switch expr among { code }

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.

···

--
Brent Royal-Gordon
Architechies


(Radek Pietruszewski) #3

+1, it’s a straightforward improvement. It’s a good thing for the language not to surprise me rejecting something that seems completely reasonable.

— Radek

···

On 23 Mar 2016, at 07:03, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I consider it to be a bug fix, but since it is a language extension, I feel that it should go through the evolution process. Thoughts appreciated!

-Chris

Introduction & Motivation

The three statements `if`, `while`, and `guard` form a family that all take a rich form of conditions that can include one or more boolean conditions, `#available` clauses, and `let`/`case` pattern bindings. These are described by the `condition-clause` production in the TSPL reference section and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions embedded in the condition, because that would be generally ambiguous with the body of an if or while statement:

  if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by performing arbitrary lookahead or by performing type checking while parsing, these approaches have significant consequences for the architecture for the compiler. As such, we’ve opted keep the parser simple and disallow this. Unrelated to this proposal, I landed a patch (https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446) which *greatly* improves the error messages in some of the most common cases where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements, it does not make sense for ‘guard': The body of a guard statement is delineated by the `else` keyword, so there is no ambiguity. A brace is always the start of a trailing closure.

From a historical perspective, the current situation was an oversight. An earlier design for `guard` did not include the `else` keyword (it used the `unless` keyword), and I forgot to fix this when we decided to resyntax it to `guard/else`.

Proposed solution

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

Detailed Design

The impact on the compiler is trivial, here’s a patch:

<guard.patch>

Impact on existing code

There is no impact on existing code. This only makes formerly invalid code start being accepted.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

Do nothing: It can be argued that this change would make guard inconsistent with the restrictions of ‘if’ and ‘while’ and that inconsistency would be confusing. On the other hand, I am arguing that this is an arbitrary restriction.

Expand the scope of “if” and “while” statements: Through enough heroics and lookahead we could consider relaxing the trailing closure requirements on `if` and `while` statements as well. While this could be interesting, it raises several ambiguity questions. For example, we need significant lookahead to realize that “a” here is not a trailing closure, since we have a closure expression being fully applied after it:

  if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but since any such direction would be compatible with this proposal, I see it as a separable potential extension on top of this basic proposal.

Change the syntax of guard: I only list this for completeness, but we could eliminate the `else` keyword, making guard more similar to `if` and `while`. I personally think that this is a really bad idea though: the guard statement is not a general `unless` statement, and its current syntax was very very carefully evaluated, iterated on, discussed, and re-evaluated in the Swift 2 timeframe. I feel that it has stood the test of time well since then.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Ilya Belenkiy) #4

+1, a very useful change. I'd use it in lots of places in my code.

···

On Wed, Mar 23, 2016 at 2:03 AM Chris Lattner via swift-evolution < swift-evolution@swift.org> wrote:

Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I
consider it to be a bug fix, but since it is a language extension, I feel
that it should go through the evolution process. Thoughts appreciated!

-Chris

*Introduction & Motivation*
The three statements `if`, `while`, and `guard` form a family that all
take a rich form of conditions that can include one or more boolean
conditions, `#available` clauses, and `let`/`case` pattern bindings. These
are described by the `condition-clause` production in the TSPL reference
section and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions
embedded in the condition, because that would be generally ambiguous with
the body of an if or while statement:

if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by
performing arbitrary lookahead or by performing type checking while
parsing, these approaches have significant consequences for the
architecture for the compiler. As such, we’ve opted keep the parser simple
and disallow this. Unrelated to this proposal, I landed a patch (
https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446)
which *greatly* improves the error messages in some of the most common
cases where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements,
it does not make sense for ‘guard': The body of a guard statement is
delineated by the `else` keyword, so there is no ambiguity. A brace is
always the start of a trailing closure.

From a historical perspective, the current situation was an oversight. An
earlier design for `guard` did not include the `else` keyword (it used the
`unless` keyword), and I forgot to fix this when we decided to resyntax it
to `guard/else`.

*Proposed solution*

The solution is simple: allow trailing closures in guard bodies. This
would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

*Detailed Design*

The impact on the compiler is trivial, here’s a patch:

*Impact on existing code*
There is no impact on existing code. This only makes formerly invalid
code start being accepted.

*Alternatives considered*
There are three primary alternatives: do nothing, expand the scope of ‘if'
and ‘while’ conditions as well, and significantly change the syntax of
guard.

*Do nothing*: It can be argued that this change would make guard
inconsistent with the restrictions of ‘if’ and ‘while’ and that
inconsistency would be confusing. On the other hand, I am arguing that
this is an arbitrary restriction.

*Expand the scope of “if” and “while” statements:* Through enough
heroics and lookahead we could consider relaxing the trailing closure
requirements on `if` and `while` statements as well. While this could be
interesting, it raises several ambiguity questions. For example, we need
significant lookahead to realize that “a” here is not a trailing closure,
since we have a closure expression being fully applied after it:

if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but
since any such direction would be compatible with this proposal, I see it
as a separable potential extension on top of this basic proposal.

*Change the syntax of guard: *I only list this for completeness, but we
could eliminate the `else` keyword, making guard more similar to `if` and
`while`. I personally think that this is a really bad idea though: the
guard statement is not a general `unless` statement, and its current syntax
was very very carefully evaluated, iterated on, discussed, and re-evaluated
in the Swift 2 timeframe. I feel that it has stood the test of time well
since then.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Sweeris) #5

+1

···

On Mar 23, 2016, at 01:03, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I consider it to be a bug fix, but since it is a language extension, I feel that it should go through the evolution process. Thoughts appreciated!

-Chris

Introduction & Motivation

The three statements `if`, `while`, and `guard` form a family that all take a rich form of conditions that can include one or more boolean conditions, `#available` clauses, and `let`/`case` pattern bindings. These are described by the `condition-clause` production in the TSPL reference section and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions embedded in the condition, because that would be generally ambiguous with the body of an if or while statement:

  if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by performing arbitrary lookahead or by performing type checking while parsing, these approaches have significant consequences for the architecture for the compiler. As such, we’ve opted keep the parser simple and disallow this. Unrelated to this proposal, I landed a patch (https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446) which *greatly* improves the error messages in some of the most common cases where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements, it does not make sense for ‘guard': The body of a guard statement is delineated by the `else` keyword, so there is no ambiguity. A brace is always the start of a trailing closure.

From a historical perspective, the current situation was an oversight. An earlier design for `guard` did not include the `else` keyword (it used the `unless` keyword), and I forgot to fix this when we decided to resyntax it to `guard/else`.

Proposed solution

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

Detailed Design

The impact on the compiler is trivial, here’s a patch:

<guard.patch>

Impact on existing code

There is no impact on existing code. This only makes formerly invalid code start being accepted.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

Do nothing: It can be argued that this change would make guard inconsistent with the restrictions of ‘if’ and ‘while’ and that inconsistency would be confusing. On the other hand, I am arguing that this is an arbitrary restriction.

Expand the scope of “if” and “while” statements: Through enough heroics and lookahead we could consider relaxing the trailing closure requirements on `if` and `while` statements as well. While this could be interesting, it raises several ambiguity questions. For example, we need significant lookahead to realize that “a” here is not a trailing closure, since we have a closure expression being fully applied after it:

  if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but since any such direction would be compatible with this proposal, I see it as a separable potential extension on top of this basic proposal.

Change the syntax of guard: I only list this for completeness, but we could eliminate the `else` keyword, making guard more similar to `if` and `while`. I personally think that this is a really bad idea though: the guard statement is not a general `unless` statement, and its current syntax was very very carefully evaluated, iterated on, discussed, and re-evaluated in the Swift 2 timeframe. I feel that it has stood the test of time well since then.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Rob Mayoff) #6

I think there will be confusion if these statements are inconsistent
regarding trailing closures.


(Rudolf Adamkovič) #7

I'm +1 and -1 at the same time.

It's handy but breaks consistency between guard and all the other control flow statements.

R+

···

Sent from my iPhone

On 23 Mar 2016, at 07:03, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I consider it to be a bug fix, but since it is a language extension, I feel that it should go through the evolution process. Thoughts appreciated!

-Chris

Introduction & Motivation

The three statements `if`, `while`, and `guard` form a family that all take a rich form of conditions that can include one or more boolean conditions, `#available` clauses, and `let`/`case` pattern bindings. These are described by the `condition-clause` production in the TSPL reference section and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions embedded in the condition, because that would be generally ambiguous with the body of an if or while statement:

  if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by performing arbitrary lookahead or by performing type checking while parsing, these approaches have significant consequences for the architecture for the compiler. As such, we’ve opted keep the parser simple and disallow this. Unrelated to this proposal, I landed a patch (https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446) which *greatly* improves the error messages in some of the most common cases where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements, it does not make sense for ‘guard': The body of a guard statement is delineated by the `else` keyword, so there is no ambiguity. A brace is always the start of a trailing closure.

From a historical perspective, the current situation was an oversight. An earlier design for `guard` did not include the `else` keyword (it used the `unless` keyword), and I forgot to fix this when we decided to resyntax it to `guard/else`.

Proposed solution

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

Detailed Design

The impact on the compiler is trivial, here’s a patch:

<guard.patch>

Impact on existing code

There is no impact on existing code. This only makes formerly invalid code start being accepted.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

Do nothing: It can be argued that this change would make guard inconsistent with the restrictions of ‘if’ and ‘while’ and that inconsistency would be confusing. On the other hand, I am arguing that this is an arbitrary restriction.

Expand the scope of “if” and “while” statements: Through enough heroics and lookahead we could consider relaxing the trailing closure requirements on `if` and `while` statements as well. While this could be interesting, it raises several ambiguity questions. For example, we need significant lookahead to realize that “a” here is not a trailing closure, since we have a closure expression being fully applied after it:

  if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but since any such direction would be compatible with this proposal, I see it as a separable potential extension on top of this basic proposal.

Change the syntax of guard: I only list this for completeness, but we could eliminate the `else` keyword, making guard more similar to `if` and `while`. I personally think that this is a really bad idea though: the guard statement is not a general `unless` statement, and its current syntax was very very carefully evaluated, iterated on, discussed, and re-evaluated in the Swift 2 timeframe. I feel that it has stood the test of time well since then.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Andrey Tarantsov) #8

+1, but

Expand the scope of “if” and “while” statements

+2 for this. At least a dozen times I've been beaten by changing

let foo = bar.map { ...}

into

if let foo = bar.map { ...} {
    ...
}

only to find myself with a compiler error (because my brain just doesn't register this transformation as invalid).

I would be fine with limiting such a call to a single line, because a multiline scenario looks crazy anyway. I would also be fine with disallowing single-line IFs in this case, because, again, they look crazy. Those two limitations would allow unambiguous parsing in all cases.

And yay for unbounded lookahead and heroics. We should use all that RAM and CPUs for something, after all.

A.


(Erica Sadun) #9

I don't think this issue arises if you use the rule that functional closure arguments (returning values) shouldn't trail:

func f(arr : [Int]?) {
    guard let x = arr?.map({ $0+1 }) else { // compiles
        preconditionFailure()
    }
}

That said, this seems like a positive tweak and I have no problem with its adoption.

-- E

···

On Mar 23, 2016, at 3:52 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
guard let x = arr?.map {$0+1} else {
   preconditionFailure()
}

// ...
}

This sounds perfectly reasonable.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

  if expr then { code }
  while expr do { code }
  for elem in expr do { code }
  switch expr among { code }

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Erica Sadun) #10

I don't believe the typical Swift user categorizes them together or would notice any kind of cognitive dissonance.
This is a courtesy improvement that adds a better developer experience and probably won't ever be noticed by
the vast majority of adopters.

-- E

···

On Mar 23, 2016, at 1:17 PM, Rob Mayoff via swift-evolution <swift-evolution@swift.org> wrote:

I think there will be confusion if these statements are inconsistent regarding trailing closures.


(Haravikk) #11

+1, a very useful change. I'd use it in lots of places in my code.

Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I consider it to be a bug fix, but since it is a language extension, I feel that it should go through the evolution process. Thoughts appreciated!

-Chris

Introduction & Motivation

The three statements `if`, `while`, and `guard` form a family that all take a rich form of conditions that can include one or more boolean conditions, `#available` clauses, and `let`/`case` pattern bindings. These are described by the `condition-clause` production in the TSPL reference section and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions embedded in the condition, because that would be generally ambiguous with the body of an if or while statement:

  if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by performing arbitrary lookahead or by performing type checking while parsing, these approaches have significant consequences for the architecture for the compiler. As such, we’ve opted keep the parser simple and disallow this. Unrelated to this proposal, I landed a patch (https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446) which *greatly* improves the error messages in some of the most common cases where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements, it does not make sense for ‘guard': The body of a guard statement is delineated by the `else` keyword, so there is no ambiguity. A brace is always the start of a trailing closure.

From a historical perspective, the current situation was an oversight. An earlier design for `guard` did not include the `else` keyword (it used the `unless` keyword), and I forgot to fix this when we decided to resyntax it to `guard/else`.

Proposed solution

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

Detailed Design

The impact on the compiler is trivial, here’s a patch:

Impact on existing code

There is no impact on existing code. This only makes formerly invalid code start being accepted.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

Do nothing: It can be argued that this change would make guard inconsistent with the restrictions of ‘if’ and ‘while’ and that inconsistency would be confusing. On the other hand, I am arguing that this is an arbitrary restriction.

Expand the scope of “if” and “while” statements: Through enough heroics and lookahead we could consider relaxing the trailing closure requirements on `if` and `while` statements as well. While this could be interesting, it raises several ambiguity questions. For example, we need significant lookahead to realize that “a” here is not a trailing closure, since we have a closure expression being fully applied after it:

  if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but since any such direction would be compatible with this proposal, I see it as a separable potential extension on top of this basic proposal.

Change the syntax of guard: I only list this for completeness, but we could eliminate the `else` keyword, making guard more similar to `if` and `while`. I personally think that this is a really bad idea though: the guard statement is not a general `unless` statement, and its current syntax was very very carefully evaluated, iterated on, discussed, and re-evaluated in the Swift 2 timeframe. I feel that it has stood the test of time well since then.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

···

On 23 Mar 2016, at 12:27, Ilya Belenkiy via swift-evolution <swift-evolution@swift.org> wrote:
On Wed, Mar 23, 2016 at 2:03 AM Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Haravikk) #12

Apologies for the empty message, somehow managed to send by accident.

Anyway, I wanted to mention that I’m uncertain about this proposal, as I’ve actually found myself using trailing closures less and less due to their somewhat ambiguous syntax (visually I mean), and when I do use them I do the following:

  let foo = myArray.map(){ $0 + 1 }

i.e- to clarify that it’s a method and not a property.

The only real advantage to them is not having to include the label, but that’s something we can omit anyway, and the parenthesis aren’t exactly burdensome to include around the closure. So I’m actually starting to lose my initial enthusiasm for trailing closures I think, as writing myArray.map({$0 + 1}) isn’t exactly costing me much, but is more explicit about what’s going on. I meant even be pro removal of trailing closures, not sure yet.

···

On 23 Mar 2016, at 12:27, Ilya Belenkiy via swift-evolution <swift-evolution@swift.org> wrote:

+1, a very useful change. I'd use it in lots of places in my code.

On Wed, Mar 23, 2016 at 2:03 AM Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I consider it to be a bug fix, but since it is a language extension, I feel that it should go through the evolution process. Thoughts appreciated!

-Chris

Introduction & Motivation

The three statements `if`, `while`, and `guard` form a family that all take a rich form of conditions that can include one or more boolean conditions, `#available` clauses, and `let`/`case` pattern bindings. These are described by the `condition-clause` production in the TSPL reference section and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions embedded in the condition, because that would be generally ambiguous with the body of an if or while statement:

  if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by performing arbitrary lookahead or by performing type checking while parsing, these approaches have significant consequences for the architecture for the compiler. As such, we’ve opted keep the parser simple and disallow this. Unrelated to this proposal, I landed a patch (https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446) which *greatly* improves the error messages in some of the most common cases where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements, it does not make sense for ‘guard': The body of a guard statement is delineated by the `else` keyword, so there is no ambiguity. A brace is always the start of a trailing closure.

From a historical perspective, the current situation was an oversight. An earlier design for `guard` did not include the `else` keyword (it used the `unless` keyword), and I forgot to fix this when we decided to resyntax it to `guard/else`.

Proposed solution

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

Detailed Design

The impact on the compiler is trivial, here’s a patch:

Impact on existing code

There is no impact on existing code. This only makes formerly invalid code start being accepted.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

Do nothing: It can be argued that this change would make guard inconsistent with the restrictions of ‘if’ and ‘while’ and that inconsistency would be confusing. On the other hand, I am arguing that this is an arbitrary restriction.

Expand the scope of “if” and “while” statements: Through enough heroics and lookahead we could consider relaxing the trailing closure requirements on `if` and `while` statements as well. While this could be interesting, it raises several ambiguity questions. For example, we need significant lookahead to realize that “a” here is not a trailing closure, since we have a closure expression being fully applied after it:

  if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but since any such direction would be compatible with this proposal, I see it as a separable potential extension on top of this basic proposal.

Change the syntax of guard: I only list this for completeness, but we could eliminate the `else` keyword, making guard more similar to `if` and `while`. I personally think that this is a really bad idea though: the guard statement is not a general `unless` statement, and its current syntax was very very carefully evaluated, iterated on, discussed, and re-evaluated in the Swift 2 timeframe. I feel that it has stood the test of time well since then.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Thorsten Seitz) #13

+1
Some more comments inline.

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
guard let x = arr?.map {$0+1} else {
   preconditionFailure()
}

// ...
}

This sounds perfectly reasonable.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

   if expr then { code }
   while expr do { code }
   for elem in expr do { code }
   switch expr among { code }

Switch does not need a new keyword, as it already has 'case'.
Adding 'then' to if statements would easily allow if-then-else expressions without braces by just replacing the blocks with expressions.

-Thorsten

···

Am 23.03.2016 um 10:52 schrieb Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org>:

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #14

To be fair, mandatory `else` breaks the symmetry between `guard` and
other control statements. Given that it's not symmetrical as it is,
I'm +1 on the proposal.

···

On Wed, Mar 23, 2016 at 6:06 PM, Rudolf Adamkovic via swift-evolution <swift-evolution@swift.org> wrote:

I'm +1 and -1 at the same time.

It's handy but breaks consistency between guard and all the other control
flow statements.

R+

Sent from my iPhone

On 23 Mar 2016, at 07:03, Chris Lattner via swift-evolution > <swift-evolution@swift.org> wrote:

Hi everyone,

This is a proposal for a very narrow extension to the guard statement. I
consider it to be a bug fix, but since it is a language extension, I feel
that it should go through the evolution process. Thoughts appreciated!

-Chris

Introduction & Motivation

The three statements `if`, `while`, and `guard` form a family that all take
a rich form of conditions that can include one or more boolean conditions,
`#available` clauses, and `let`/`case` pattern bindings. These are
described by the `condition-clause` production in the TSPL reference section
and as a `stmt-condition` in the compiler source code.

Today, these do not permit trailing closures in any top-level expressions
embedded in the condition, because that would be generally ambiguous with
the body of an if or while statement:

if foo { // start of trailing closure, or start of the if body?

While it would be possible to tell what is intended in some cases by
performing arbitrary lookahead or by performing type checking while parsing,
these approaches have significant consequences for the architecture for the
compiler. As such, we’ve opted keep the parser simple and disallow this.
Unrelated to this proposal, I landed a patch
(https://github.com/apple/swift/commit/30ec0f4128525a16f998e04ae8b1f70180627446)
which *greatly* improves the error messages in some of the most common cases
where a developer accidentally tries to do this.

However, while this approach makes sense for `if` and `while` statements, it
does not make sense for ‘guard': The body of a guard statement is delineated
by the `else` keyword, so there is no ambiguity. A brace is always the
start of a trailing closure.

From a historical perspective, the current situation was an oversight. An
earlier design for `guard` did not include the `else` keyword (it used the
`unless` keyword), and I forgot to fix this when we decided to resyntax it
to `guard/else`.

Proposed solution

The solution is simple: allow trailing closures in guard bodies. This would
allow this silly example to compile correctly:

func f(arr : [Int]?) {
  guard let x = arr?.map {$0+1} else {
    preconditionFailure()
  }

  // ...
}

Detailed Design

The impact on the compiler is trivial, here’s a patch:

<guard.patch>

Impact on existing code

There is no impact on existing code. This only makes formerly invalid code
start being accepted.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if'
and ‘while’ conditions as well, and significantly change the syntax of
guard.

Do nothing: It can be argued that this change would make guard inconsistent
with the restrictions of ‘if’ and ‘while’ and that inconsistency would be
confusing. On the other hand, I am arguing that this is an arbitrary
restriction.

Expand the scope of “if” and “while” statements: Through enough heroics and
lookahead we could consider relaxing the trailing closure requirements on
`if` and `while` statements as well. While this could be interesting, it
raises several ambiguity questions. For example, we need significant
lookahead to realize that “a” here is not a trailing closure, since we have
a closure expression being fully applied after it:

if foo { …a... } { …b… }()

this could be addressed with whitespace rules or other approaches, but since
any such direction would be compatible with this proposal, I see it as a
separable potential extension on top of this basic proposal.

Change the syntax of guard: I only list this for completeness, but we could
eliminate the `else` keyword, making guard more similar to `if` and `while`.
I personally think that this is a really bad idea though: the guard
statement is not a general `unless` statement, and its current syntax was
very very carefully evaluated, iterated on, discussed, and re-evaluated in
the Swift 2 timeframe. I feel that it has stood the test of time well since
then.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #15

Thanks, I’ll add this to the end of the proposal!

-Chris

···

On Mar 23, 2016, at 2:52 AM, Brent Royal-Gordon <brent@architechies.com> wrote:

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
guard let x = arr?.map {$0+1} else {
   preconditionFailure()
}

// ...
}

This sounds perfectly reasonable.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

  if expr then { code }
  while expr do { code }
  for elem in expr do { code }
  switch expr among { code }

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.


#16

Could such new keywords be optional, only required when the compiler see the construct as ambiguous? For simple construct, these new then/do/do/among are in a way useless; and it would be bad to impose them on all to support a rare use case.

Dany

···

Le 23 mars 2016 à 05:52, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> a écrit :

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
guard let x = arr?.map {$0+1} else {
   preconditionFailure()
}

// ...
}

This sounds perfectly reasonable.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

  if expr then { code }
  while expr do { code }
  for elem in expr do { code }
  switch expr among { code }

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.


(Thorsten Seitz) #17

I don't think this issue arises if you use the rule that functional closure arguments (returning values) shouldn't trail:

I'm not very fond of that rule, so I'd prefer the solution proposed by Chris :slight_smile:

-Thorsten

···

Am 23.03.2016 um 13:11 schrieb Erica Sadun via swift-evolution <swift-evolution@swift.org>:

func f(arr : [Int]?) {
   guard let x = arr?.map({ $0+1 }) else { // compiles
       preconditionFailure()
   }
}

That said, this seems like a positive tweak and I have no problem with its adoption.

-- E

On Mar 23, 2016, at 3:52 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

The solution is simple: allow trailing closures in guard bodies. This would allow this silly example to compile correctly:

func f(arr : [Int]?) {
guard let x = arr?.map {$0+1} else {
  preconditionFailure()
}

// ...
}

This sounds perfectly reasonable.

Alternatives considered

There are three primary alternatives: do nothing, expand the scope of ‘if' and ‘while’ conditions as well, and significantly change the syntax of guard.

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

   if expr then { code }
   while expr do { code }
   for elem in expr do { code }
   switch expr among { code }

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #18

No. If the compiler could tell when the keyword was necessary, we just wouldn’t require it! :-)

The other approach you could take is to only use this sort of keyword when a trailing closure is present, but at that point, you might as well just wrap the trailing closure with parentheses (which is already supported today, and the compiler now nudges you towards).

-Chris

···

On Mar 23, 2016, at 8:11 PM, Dany St-Amant via swift-evolution <swift-evolution@swift.org> wrote:

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

  if expr then { code }
  while expr do { code }
  for elem in expr do { code }
  switch expr among { code }

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.

Could such new keywords be optional, only required when the compiler see the construct as ambiguous?


(Xiaodi Wu) #19

Certainly, `while expr do { code }` is clever, but it could still get hairy:

func foo(_ bar: Int) -> Bool {
  return true
}

func foo(_ bar: Int, baz: () -> Void) -> Bool {
  return false
}

// what's going on here?
while foo(42) {
  code
}
do {
  much code
}
catch {
  code
}

You've got to read pretty far to know which foo is called! Let's say
we stipulate that we'll use another word instead of `do`. Maybe we
choose something not a reserved keyword: `while expr frobnicate { code
}`. Better, until you have a function `frobnicate(closure: () ->
Void)`, and then you could run into problems again. Maybe we stipulate
that `do` must be on the same line as the closing brace. Well, first
off, that would be inconsistent. It was settled on this list a while
back, I believe, that the preferred style in Swift is:

if expr {
  code
}
else { // else on its own line
  code
}

Likewise, I'm allowed to write (although I recognize that this isn't
the most common style in which it's written):

guard expr
else {
  code
}

So the same-line rule would be inconsistent. Suppose we forbid
newlines before else for consistency--even then, I would argue that
clarity for the human reader is not greatly improved. I'm not yet
convinced that trailing closures in these situations are so desirable
as to be worth this syntactic muddling. Limiting trailing closures to
guard expressions alone doesn't raise any of these issues.

···

On Wed, Mar 23, 2016 at 11:54 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 23, 2016, at 8:11 PM, Dany St-Amant via swift-evolution <swift-evolution@swift.org> wrote:

A fourth alternative would be to change the syntaxes of `if` and `while` (and probably `for` and `switch`) to also have a keyword in this position.

     if expr then { code }
     while expr do { code }
     for elem in expr do { code }
     switch expr among { code }

I'm not going to say I advocate for this option, but it *would* clearly mark the end of the condition so that trailing closures could be brought to all of these statements, so it seemed worth mentioning.

Could such new keywords be optional, only required when the compiler see the construct as ambiguous?

No. If the compiler could tell when the keyword was necessary, we just wouldn’t require it! :slight_smile:

The other approach you could take is to only use this sort of keyword when a trailing closure is present, but at that point, you might as well just wrap the trailing closure with parentheses (which is already supported today, and the compiler now nudges you towards).

-Chris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution