`if let` shorthand

Shorthand syntax for optional binding conditions that shadow an existing variable (e.g. if let foo = foo) has come up many times over the years. Most recently, it was discussed in Let's fix if let syntax. I felt like the reception in that thread was reasonably positive, so I implemented support for this in apple/swift#40694. Here's a pitch (full proposal document here):

Introduction

Optional binding using if let foo = foo { ... }, to create an unwrapped variable that shadows an existing optional variable, is an extremely common pattern. This pattern requires the author to repeat the referenced identifier twice, which can cause these optional binding conditions to be verbose, especialy when using lengthy variable names. We should introduce a shorthand syntax for optional binding when shadowing an existing variable:

let foo: Foo? = ...

if let foo {
    // `foo` is of type `Foo`
}

Motivation

Reducing duplication, especially of lengthy variable names, makes code both easier to write and easier to read.

For example, this statement that unwraps someLengthyVariableName and anotherImportantVariable is rather arduous to read (and was without a doubt arduous to write):

let someLengthyVariableName: Foo? = ...
let anotherImportantVariable: Bar? = ...

if let someLengthyVariableName = someLengthyVariableName, let anotherImportantVariable = anotherImportantVariable {
    ...
}

Proposed solution

If we instead omit the right-hand expression, and allow the compiler to automatically shadow the existing variable with that name, these optional bindings are much less verbose, and noticably easier to read / write:

let someLengthyVariableName: Foo? = ...
let anotherImportantVariable: Bar? = ...

if let someLengthyVariableName, let anotherImportantVariable {
    ...
}

This is a fairly natural extension to the existing syntax for optional binding conditions. Using let (or var) here makes it abundantly clear that a new variable is being defined, which is especially important when used with mutable value types. Using let / var here also allows us to avoid adding any new keywords to the language.

Detailed design

Specifically, this proposal extends the Swift grammar for optional-binding-conditions.

This is currently defined as:

optional-binding-condition → let pattern initializer | var pattern initializer

and would be updated to:

optional-binding-condition → let pattern initializeropt | var pattern initializeropt

This would apply to all conditional control flow statements:

if let foo { ... }
if var foo { ... }

else if let foo { ... }
else if var foo { ... }

guard let foo else { ... }
guard var foo else { ... }

while let foo { ... }
while var foo { ... }

The compiler would synthesize an initializer expression that references the variable being shadowed.

For example:

if let foo { ... }

is transformed into:

if let foo = foo { ... }
67 Likes

I’m sure this has been floated before, although I can’t find a link and I don’t think it ever hit formal review. But I’d support this for sure, typing out that the same thing equals the same thing is no more syntactically meaningful than the proposed ‘if let x’ and the extra characters just seem like unnecessary noise.

4 Likes

This feature is listed as a commonly rejected feature under Syntactic sugar for if let self-assignment at swift-evolution/commonly_proposed.md at main · apple/swift-evolution · GitHub.
However, this is now ~5 years old.

@Ben_Cohen discussed this somewhat recently in a reply to Let's fix if let syntax:

So I get the impression that there is some openness to considering this sort of proposal.

I agree with @bzamayo's point here:

imho this proposed syntax doesn't "favor terseness over clarity" because it is equally as clear as the existing syntax (or at least isn't any more "magical" than the existing syntax). And if it's equally / sufficiently clear, then reducing duplication seems like a clear readability win.

3 Likes

Heh. Another one of the commonly-rejected proposals?

  • Rewrite the Swift compiler in Swift: This would be a lot of fun someday, but (unless you include rewriting all of LLVM) requires the ability to import C++ APIs into Swift. Additionally, there are lots of higher priority ways to make Swift better.

Whoopsie!

Anyway, +1 to this. I'd use it.

Indeed, there’s no absolute bar to considering a topic.

However, there really does need to be something new (a new perspective not before considered, a new solution, something else?).

Merely reopening a discussion due to the passage of time to say that what’s been already discussed should be un-rejected doesn’t meet that bar, methinks.

For the pros and cons of this particular suggestion, it suffices to refer to the existing record on these forums.

2 Likes

If the " = foo" part considered noise in:
if let foo = foo {
foo.bar()
}
then why leave the "let " in there? Shall this be completely "noise free"?
if foo {
foo.bar()
}

Edit: on the second thought I don't think that was a good idea, scratching out.

I don't think the let is noise -- it indicates that a new, separate variable is being declared for the inner scope, which is a key part of Swift's optional binding semantics (not a huge deal for let declarations, but very meaningful for var declarations).

if let foo { ... } also naturally extends to if var foo { ... } as shorthand for if var foo = foo { ... }, which seems important -- let and var are effectively peers with respect to variable declarations, so any new syntax should support both.

8 Likes

We already have a similar precedent:

for i in 0 ..< 10 {}

vs:

for var i in 0 ..< 10 {}

so I don't see why can't we treat: "if foo {}" as "if let foo = foo {}" while treating "if var foo {}" as "if var foo = foo {}". It's a matter of convention.

3 Likes

I think you need the let there to keep it from looking like a test of a boolean condition and also to emphasize that something is being assigned.

I personally don't much care for if let foo {} but it's miles better than if foo {}

21 Likes

By this very logic we need to change "for i in range {}" to "for let i in range {}". I don't mind which way we settle upon but we must be consistent, IMHO.

2 Likes

I think it's clear in for i in blah blah blah... that i is being assigned something as you loop through whatever you're looping through in a way that if foo {} is not clear about assignment. Plus, there's no way to confuse for i in with a boolean condition. An implicit assignment is part and parcel of what for ... in ... does; the same doesn't hold true for if.

6 Likes

that i is getting defined in the inner scope of for, that it is defined as an immutable variable (while not being prefixed with an explicit let - all that is a matter of convention. If we didn't have "for i in ..." syntax today and were designing it along with this if let feature, would we end up having two different syntaxes? I don't think so. `if foo' is kind of boolean test anyway, you can read it as "if there is foo", which is what it essentially is.

This is an interesting point, thanks for mentioning this.

I do agree with this -- if if foo { ... } could be either a boolean condition or an optional binding, then it could lead to some confusing / counter-intuitive situations:

let foo: Bool = true
let bar: Bool? = false

if foo, bar {
  // would succeed
}

if foo == true, bar == true {
  // would fail
}

It seems more clear for boolean conditions and optional bindings to have different spellings:

if foo, let bar {
  ...
}
11 Likes

This is the same point made when Swift 1 removed implicit Bool? checking, so not only would such a change need to be independently justified, it would need to justify reversing a decision made seven years ago as well.

6 Likes

I still think this shorthand is best spelled as if let foo?, by analogy to if case let foo? = foo.

10 Likes

Have we considered the idea of introducing a new unwrap keyword?

var foo: Int?

unwrap foo {
    // `foo` has been shadowed by the unwrapped value
} else {
    // Unwrapping failed
}

This could be used with multiple variables:

unwrap foo, bar {
    // Neither `foo` nor `bar` were nil.
} else {
    // Either `foo`, `bar` or both were nil.
}
8 Likes

1+

1 Like

+1

1 Like

With unwrap you are losing other things you would otherwise be able doing, the end result is less concise, example:

if let foo, foo.isActive {
    bar(foo)
}

// vs

unwrap foo {
    if foo.isActive {
        bar(foo)
    }
}
12 Likes