SE-0345: `if let` shorthand for shadowing an existing optional variable

I concur with all your points - I don't want to see this sort of "symbol salad" proliferate, and I don't like that if let foo makes very little sense. But while I understand where you're coming from, I disagree with your conclusion. Given that long-form if let already exists in the language, and will remain so, I think this leverages the existing "symbol salad" effectively to avoid raising the overall learning curve, while offering more than an "unwrap" keyword (a new keyword to learn) could offer.

Edit: Consider this part of my review. I don't mean to say @jpmhouston 's review is incorrect, misinformed, or anything like that, but rather highlight the difference and similarities in thinking from each side. :slightly_smiling_face:

I kind prefer if let value? because it's already used by switch statements in a similar way:

var value: Int? = 10

switch value {
    case let value?:
        print(value) // Int
    case nil:
        print("nil")
}
34 Likes

Just wanted to elaborate a bit on why I'm not super compelled by the proposal's arguments against if let foo?

if let foo = foo (the most common existing syntax for this) unwraps optionals without an explicit ? . This implies that a conditional optional binding is sufficiently clear without a ? to indicate the presence of an optional. If this is the case, then an additional ? is likely not strictly necessary in the shorthand if let foo case.

I of course agree that the question mark is not strictly necessary, but I don't really know what "strictly necessary" would mean besides a formal grammatical ambiguity. Yes, the existing construct unwraps without a question mark, but a) some have cited this as a drawback of existing if let x = y syntax (which I agree with) and b) when we condense the expression to if let x it seems reasonable to me that we would want some additional structure for clarity. The problem we're mainly trying to solve is the repetition of lengthy variable names, not "too many tokens."

// Consistent
if let user, let defaultAddress = user.shippingAddresses.first { ... }

// Inconsistent
if let user?, let defaultAddress = user.shippingAddresses.first { ... }

I don't buy that "consistency" is violated only by the addition of tokens—the first version above is already inconsistent because it has an equals sign and a RHS expression. The second version doesn't really feel more inconsistent to me.

Additionally, the ? symbol makes it trickier to support explicit type annotations like in if let foo: Foo = foo . if let foo: Foo is a natural consequence of the existing grammar. It's less clear how this would work with an additional ? . if let foo?: Foo likely makes the most sense, but doesn't match any existing language constructs.

This just... doesn't seem like that big of a problem to me? I actually don't even know if we should allow type annotations in this position. IMO

if let foo: Foo { ... }

does not read as an optional unwrap like if let foo? or if let foo does. I can't off the top of my head think of what functionality this actually enables—is there a good example the author could provide? Anyway, even in the face of a compelling use case, I'm just not that troubled if the answer is "if you need this uncommon and more verbose pattern, just use the longhand if let foo: Foo = foo form."

ETA: I guess the type annotation syntax supports constructs like:

class B {}
class C: B {}

let c: C? = C()

if let b: B = c {
    print(b)
}

but as I think more about it I think we should not allow this construct in the shorthand form. We don't allow such an equivalent in the capture list, and allowing the type to be changed defeats the "simple" model for if let foo: that the inner foo is the same type as the outer foo just unwrapped.

15 Likes

Yes -- a hypothetical future if inout &foo syntax would work in the same way as Kotlin's type-refinement syntax. Mutations in the inner scope would also apply to the outer scope.

More discussion about potential upcoming borrow variables can be found in "A roadmap for improving Swift performance predictability".

// Kotlin, type refinement
var foo: String? = "foo"
print(foo?.length) // 3

if (foo != null) {
    foo = "baaz"
    print(foo.length) // 4
}

print(foo?.length) // 4
// Swift, `if var`
var foo: String? = "foo"
print(foo?.count) // 3

// creates a separate copy, that is mutable:
if if var foo {
    foo = "baaz"
    print(foo.count) // 4
}

print(foo?.count) // 3
// Swift, hypothetical `if inout`
var foo: String? = "foo"
print(foo?.cound) // 3

// borrows and mutates the storage of the existing foo variable
if inout &foo {
    foo = "baaz"
    print(foo.count) // 4
}

print(foo?.count) // 4
2 Likes

IMO this is an even more compelling case for if let foo?. I think if inout &foo? is more clear about the fact that we are borrowing the wrapped value, not the optional value as a whole. I also think if var foo? is better—I don't have the same instinct that if var unwraps an optional as I do for if let and this goes double for any potential future extensions like if ref foo and if inout &foo.

I realize these are hypothetical future syntaxes, but we've chosen to review this proposal now before the design of these other introducers is settled. We should try to accept a design which will compose well.

7 Likes

Agreed that most of the style-related arguments here are pretty subjective. For the default case I think if let foo and if let foo? are both totally reasonable.

I do think if let foo meshes more nicely with other syntax / features adjacent to optional unwrapping. Support for explicit type annotations is one example currently included in the proposal / implementation:

if let foo: Foo { ... }

This falls out completely naturally from the change to the grammar, which is to just make the RHS optional. Disallowing this here would make the grammar a bit more complicated / less generalized.

For consistency with other lets / vars in the language, it seems desirable to support type annotations here unless we have a really compelling reason to disallow them. This was brought up in the pitch thread, as one example.

A future direction mentioned in the proposal is support for optional casting:

if let foo as? Bar { ... }

// as potential future shorthand for 
if let foo = foo as? Bar { ... }

I think both of these syntax extensions are a a bit more unnatural with an additional ? symbol:

if let foo?: Foo { ... }

if let foo? as? Bar { ... }
2 Likes

I agree. +1 on the feature, preference for the trailing question mark as a link to pattern matching.

2 Likes
  • What is your evaluation of the proposal?
    +1

  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes

  • Does this proposal fit well with the feel and direction of Swift?
    Yes

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    I read the proposal in-depth and have followed and participated in the pitch thread. I have also read various threads over the years that propose something similar.

I prefer if let foo (and its cousin guard let foo) to all of the other proposed spellings. To me this proposal feels analogous to many of the closure syntax shorthands where items are able to be omitted with no sigil added to indicate the omission.

One thing I would note is that currently the if let foo = foo idiom is not mentioned at all in The Swift Programming Language book.

I think that, similar to how the book walks through the various shorthands that can be used with closures, it would be great to have a similar thing in the section that introduces optional bindings:

  • Starts with if let foo = bar as it currently does.
  • Introduces the Swift idiom if let foo = foo and explains the new variable shadows the original.
  • Introduces the if let foo shorthand.

I think including the common if foo = foo idiom when introducing optional binding would be very helpful to all newcomers to Swift regardless of whether this proposal is accepted, but would be particularly important to add it to explain this new shorthand if adopted.

3 Likes

FWIW, I'm very uncompelled by the goal of keeping the formal grammar as simple as possible compared to producing a good language model.

As I mentioned before, I think if let foo: Bar really starts to depart from the simple model of if let foo. It doesn't read as an optional unwrapping nearly as naturally to me. It's not even clear to me that it's particularly desirable to maintain the same name while changing the type—in this example, wouldn't the long-hand version be more natural as "if let bar: Bar = foo"?

On the flip side, that the syntax seems slightly more unnatural doesn't worry me. I don't think I've ever seen an if let with a type annotation 'in the wild', and as with the generics proposals, I don't think this sugar needs (or even should) support every possible construct where a user might write the token sequence foo = foo. When there's more going on than a simple unwrapping, I think comprehension is hurt by trying to cram everything into as few tokens as possible (e.g., rebinding, dynamic cast/type coercion, and optional unwrap). That if let foo more naturally supports these extensions just doesn't feel like a feature to me.

9 Likes

This already exists with if let foo = foo since just plain let foo = foo is not valid Swift. This hasn't seemed to be a significant problem with people not able to trust what let and var mean.

That said, I agree that if let a = b may not be the optimal syntax for optional binding. Newcomers need to learn that if let and if var are not the same as just let and var. But as I describe in more detail below, I think if let would be difficult to move away from at this point.

One drawback is that it doesn't allow the choice between creating an immutable or (much more rare) mutable unwrapped variable that let and var provide.

But beyond that, I think introducing a new keyword for this purpose introduces more complexity than it resolves.

If the keyword is only used as a synonym for if let x = x , then it needs to be mixed and matched with if let -style bindings when multiple items are bound in a single statement:

if unwrap user, let defaultAddress = user.shippingAddresses.first { }

This is easily a point of confusion to people new to the language, but it would be a natural question for everyone to ask "Why can't I use unwrap in both cases? Why do I have to use let sometimes and unwrap sometimes?"

It wouldn't seem to make much sense to be able to use unwrap in one case and not the other. To fix this friction, a next step would be to allow unwrap instead of let for optional binding:

if unwrap user, unwrap defaultAddress = user.shippingAddresses.first { }

And, on its own, I think that reads well.

But, without a source breaking change, if unwrap would always need to exist alongside the existing if let . Some projects will prefer one or the other, essentially creating two dialects of Swift for an incredibly common construct. And to move between codebases, everyone will need to know and understand both syntaxes (and if working on different projects keep track of which project prefers which style).

I think in practice both the "mix and match" style and the "both coexist" style would add a good deal of unwanted complexity over the current state of things and over the proposed solution.

6 Likes

What is your evaluation of the proposal?

Is it cheating to say +∞?

Is the problem being addressed significant enough to warrant a change to Swift?

Not only is it a significant enough change for Swift, it's a significant change for my source code. I suspect a lot refactoring of if let x = x code is in my future.

Does this proposal fit well with the feel and direction of Swift?

One of the early goals of Swift was to make a language that was approachable both to developers coming from other languages and, more importantly, to beginners. I think this proposal helps with that by being more readable and an gentler introduction into optional values.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I have not.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More than a quick reading, less than in-depth study.

I've been following this work since the beginning. I think @cal has done a great job of taking that initial idea and making it concrete with both a proposal and implementation.

I'm looking forward to using this new feature.

-ch

14 Likes

+∞ to what @chockenberry says.

It may be a minor point, but this is not accurate. That is legal, in similar circumstances to the proposed functionality. For example:

struct S {
  var foo: Int

  func bar() {
    let foo = foo
  }

  func baz(_ quux: Bool) {
    let quux = quux
  }
}

What's not legal is shadowing a name in the same scope.

3 Likes

I think these extension could still look natural with the if let foo? syntax:

if let (foo as Foo)? {} 

// "if let foo?” is not mutually exclusive with this syntax
if let foo as? Bar {}

For what it’s worth, I don’t find if let foo?: Foo that bad, considering that most Swift users will see this code with syntax highlighting.

+1 I think this looks great and is a great fit for Swift.

For reference, in Typescript you would check for optionality this way:

if foo != null {
  // the compiler now treats foo as non optional
}

It makes sense for a language based on JS, but this accomplishes the same level of conciseness while being much more Swift like.

1 Like

Yes, I did mean in the same scope. Thank you for pointing this out.

I think my overall point remains that sometimes let foo = foo is valid Swift and sometimes it is not and that hasn't seemed to be a significant problem with people not able to trust what let and var mean.

Similarly, let a is invalid but only because the type of a cannot be inferred. let a: Int is perfectly valid leaving initialization of a to later.

3 Likes

Happy to see this in review.
The proposal is very clear, easy to understand and thank you for your effort.

  • What is your evaluation of the proposal?

Pretty nice:

It doesn't introduce new keyword nor (pre/post)-fix operator.

  • Easy to adopt.
  • It doesn't make sense to introduce new syntax specifically only for this situation (unwrap optional and shadow it) when there's one which have the same meaning and widely understood by that way (if/guard let is there to unwrap optional).
  • Also it also doesn't make sense to do it specifically at this level. switch ... > if case let > if let ... and why new keyword from here all of a sudden?

Instead, it solves the problem by introducing a new grammar, which is smart because:

  • Grammar of LHS remains intact.
    • This means you can type it: if let a: Int { ...
  • This alternative will be new confusing syntax: if let a?: Int
  • You can opt-in this feature by just omitting the RHS of your statements.
    • Also easy to revert if needed to.
  • This is opt-in.
  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes. This will allow the compiler to do the routine we manually do currently.

  • Does this proposal fit well with the feel and direction of Swift?

From the top to the bottom, yes. I'm seeing a bit of similarity in Type Inference and also in switch:

var value: Int? = 0

switch value {
case let .some(value): print(value)
case .none: break
}

// shorthand:
if let value { ...
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

No experience of a similar feature in other languages.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Throughout the pitch of this proposal and the similar discussion thread mentioned in the proposal.

1 Like

+1 for ergonomic improvement but -1 for lots-of-sugared-syntax-burden language, as a beginner swift user and developer, in my opinion swift's readability is getting worse day by day because of A LOT OF alternative syntaxes/sugared syntaxes

like the following:

guard let if let x lotsOfAnnotators lotsOfWeirdInitializers blahblahblah

and it seems no one consider simplicity and clearness of language, just adding bunch of sugars/weirdo features-that-makes-less-typing etc.

im not opposing to syntactic sugars but IMO creating lots of alternative ways (syntaxes/sugars) to do one thing is a bad approach, of course alternatives makes language more flexible but IMO it makes language less readable and less simple also (some sort of trade-off balance)

thanx
regards

2 Likes

I'd like to suggest a clarification to the Detailed Design section - the proposal states that only valid identifiers are going to be accepted but it should also not an exception - a static variable in instance context just like it does for member references e.g.:

struct S {
  static var x: Int? = 42

  func test() {
    if let x {
      print(x)
    }
  }
}

This is going to produce the following diagnostic - static member 'x' cannot be used on instance of type 'S' with an incorrect fix-it:

if let x {
       ^
       S.

Implementation has to adjust diagnostic for static member references to account for new syntax and suggest = S.x instead of just S. for reference to x.

5 Likes