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

Question. Is if let (x, y) possible and does it mean if let x = x, let y = y ?

3 Likes

That isn’t possible in the current design, it would need to be spelled if let x, let y. Cool idea though!

3 Likes

In if let foo, foo is the newly declared item, which is non-optional, which maps exactly to what it is in if let foo = foo. I would expect its definition to be the same as it currently is using the long form. (@cal please correct me if I'm wrong.)

I don't think that will cause everlasting confusion for folks, just as if let foo = bar and if let foo = foo don't.

Refactoring is something that I don't think is explicitly addressed in the proposal. The current behavior in Xcode is that each side of if let foo = foo is refactored separately.

I would imagine that similar to how there is a compiler error if an optional value being unwrapped with optional binding is changed to be a non-optional value, that if there is no matching name that a compiler error would also point out the discrepancy. It would probably make sense to add a mention of that to the proposal.

+1 as written.

It's immediately obvious to me that if let x would desugar to if let x = x, and I suspect the same applies to most people using Swift today. It could be just as easily explained to new Swift developers as if let x = x is. I don't believe any clarity is lost compared to the syntax it sugars.

The syntax seems to me like a more Swift-idiomatic way of writing if foo != nil { // foo is unwrapped here } as you've got to use the same if let keywords we've always been used to for unwrapping an Optional.

I strongly disagree with basically all of the alternatives suggested, and would rather the feature not be added at all than have another keyword added, or a ? after the label. They would make the feature either more verbose, or slightly harder to grok what's an Optional. In my view, if let x? would suggest that x is actually an Optional value in the scope that's being opened, even if the same doesn't apply when pattern matching. It makes the syntax different enough to if let x = x to make it harder to work out what's happening, in my opinion.

I've read the original thread, the SE proposal, and most of the posts in this thread. More than a quick glance, less than an in-depth study!

10 Likes

Not to stretch the analogy to absurdity, but yes: there isn't a need to redo the entire road, if you're okay with a road full of patches that will inevitably break up and become even bigger potholes given a year or two of frost and thawing.

The future directions section gives us a glimpse of what that patchy road might look like some years down the line, and I can't say I really like it. Let's have a look:

if let foo as? Bar { ... }

That looks alright enough, but the inconsistency with case patterns is growing wider. For the simple unwrapping, cases need a question mark (if case let x? = x { ... }), the proposed syntax doesn't. For the future direction, we need to use a question mark where cases need none (if case let foo as Bar = foo { ... }). No way to explain that besides historical baggage (<=> patched potholes).

And that's only the first future direction. Let's look at the next one:

if ref foo { ... }

I brought this up in the pitch thread, but I don't think the proposed syntax translates to rarer alternatives to let/var at all. I can't imagine someone without pretty extensive language knowledge could intuitively understand that foo is being unwrapped and shadowed here. It reads more like querying some information about foo ("is foo a reference?", "is foo referenced somewhere else?").

Next one:

// `mother.father.sister` is optional

if ref mother.father.sister {
  // `mother.father.sister` is non-optional and immutable
}

if inout &mother.father.sister {
  // `mother.father.sister` is non-optional and mutable
}

This is the one I like the least, because you not only need to understand that shadowing and unwrapping is happening by a single niche keyword, now we're changing types and behaviour multiple levels of nesting deep into other types. Imo this just breaks the mental model completely. I can no longer be sure that if I read something.other.specificPropertyOfTypeA that it's actually of type A and behaves in any way similar to other places I read x.specificPropertyOfTypeA.


I know this review is about the proposed feature and not about the future directions, but I believe the future directions illustrate nicely how this is a syntax that doesn't really generalize/compose and thus imo doesn't really fit into Swift. I actually think that just the proposed feature on it's own isn't very confusing and would probably do the job it's designed for adequately, but the future directions make me a strong -1.

6 Likes

fwiw case let foo as Bar (very similar to potential future direction) is already valid pattern matching syntax:

struct Bar { }
let foo: Any = Bar()

switch foo {
  case let foo as Bar:
    // foo is `Bar`
  default:
    // ...
}

One of the reasons I mentioned this potential direction in the proposal is that it shows how if let foo likely composes better with other language features compared to if let foo?:

// as an extension to `if let foo`
if let foo as? Bar { ... }

// as an extension to `if let foo?`
if let foo? as? Bar { ... }

If the extra ? is too much of a departure from existing pattern matching syntax, we could consider spelling a future feature like this as:

if let foo as Bar { ... }

(which, honestly, I rather like!)


The behavior of if ref foo { ... } seems approximately as clear as:

if ref foo = foo { ... }

which I assume would become part of the language if/when we add these borrow introducers.

Also, keep in mind that the exact spelling for these potential borrow introducers is yet to be determined and I'm sure it would be subject to pretty extensive bikeshedding before a final proposal.

I'm with @xwu on this. None of the spoofing you mention would be prevented by this proposal, since if let foo = foo syntax is still supported. And the argument for syntax highlighting is a good one. Sure code exists outside Xcode, but no one should be approving a PR or copying code from StackOverflow without testing it.

1 Like

Once ref (or whatever we end up calling it) comes into the language, what would the rationale be for having both

if ref foo = foo { ... }

and

if let foo = foo { ... }

Wouldn’t the if let be entirely subsumed by the if ref?

What is your evaluation of the proposal?
-1 for the current implementation

This proposal as written allows unwrapping members with implicit self, but not explicit self. As far as I can tell, there is no precedent in the Swift language for this. All current features that support implicit self also support explicit self.

Personally I think it was a mistake to add implicit self to Swift in the first place, but up until now it has only been treated as a convenience, not the default.

if let self.foo {
    // this is not allowed
}

if let foo {
    // this is
}

// I see no reason why self.foo and foo should be treated differently here when they are both referencing the same member on self

Is the problem being addressed significant enough to warrant a change to Swift?
I think so. This has always felt like a natural extension of the existing syntax to me.

Does this proposal fit well with the feel and direction of Swift?
No, for the reasons stated above. If it is changed to allow explicit self wherever implicit self is allowed, then yes I think it would fit more naturally into the language.

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

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I followed the pitch thread but did not participate. I also have read through most of the review thread.

3 Likes

Wouldn’t the if let be entirely subsumed by the if ref?

This is discussed some in the proposal here:

Instead of being shorthand for if let, this new shorthand syntax could instead be shorthand for if ref. This would improve performance in general, and could nudge users towards using borrows instead of copies (since only the borrow form would receive shorthand sugar).

A key downside of borrows, however, is that they require exclusive access to the borrowed variable. Memory exclusivity violations will result in compiler errors in some cases, but can also manifest as runtime errors in more complex cases. For example:

var x: Int? = 1

func increment(by number: Int) {
    x? += number
}

if ref x = x {
    increment(by: x)
}

This would trap at runtime, because increment(by:) would attempt to modify the value of x while it is already being borrowed by the if ref x = x optional binding condition.

Borrow introducers will be very useful, but adopting them is a tradeoff between performance and conceptual overhead. Borrows are cheap but come with high conceptual overhead. Copies can be expensive but always work as expected without much extra thought. Given this tradeoff, it likely makes sense for this shorthand syntax to provide a way for users to choose between performing a copy or performing a borrow, rather than limiting users to one or the other.

I personally expect that let / var will continue to be more common than ref / inout, since borrows come with additional conceptual overhead and copies always "just work".

1 Like

Mmm, and here we've got a perfect demonstration of what I've been droning on about for years, making the case empirically that (a) the proposed syntax is less clear that the status quo; (b) that it will be continually confused with something that it is not; and that (c) the repetition of x = x is load bearing and not just extraneous cruft.

For you see, unwrapping by shadowing (if let x = x) is not the same thing as unwrapping by flow-sensitive typing (if x != nil), if the latter were to exist in Swift. This can be most clearly demonstrated in the case of if var (which is explicitly a part of this proposal) with examples that already work in today's Swift:

var x: Int? = 42
if x != nil {
  x = 0
}
print(x) // prints "Optional(0)"

var y: Int? = 42
if var y = y {
  y = 0
}
print(y) // prints "Optional(42)"

In today's Swift, that there is an independent copy of y which is being mutated is abundantly clear, while that there is not an independent copy of x which is being mutated is also abundantly clear. Now consider what's proposed here:

var z: Int? = 42
if var z {
  z = 0
}
print(z) // prints ???

In the same breath as writing that this code involving z "obvious[ly]" desugars to the same thing as the code involving y, @rhysmorgan also writes that it seems like an idiomatic way of writing the code involving xwhich is not at all the same!

Put this way, I regret saying that the proposed feature would be fine if it already existed: I actually surmise it'd be rather a pain point for the reason illustrated above, and I wouldn't recommend it.

17 Likes

No, I disagree.

I think it's clear that you're making an independent copy, because of the var/let keyword in if let x. I think it's clear in your if var z example that you'd be mutating the independent copy of z.

The exact same confusion could reasonably apply to if var z = z { z = 0 }.

I know that the behaviour of

if (x != nil) {
  // x is unwrapped
}

is different behaviourally to

if let x {
  // x is unwrapped
}

entirely because of the let keyword. I think that's what makes it clear that you're using an independent copy.

4 Likes
var z: Int? = 42
if var z {
  z = 0
}
print(z) // prints ???

That prints Optional(42), intuitively I thought. Because that z = 0 assignment is in scope and moreover if var intensifies the meaning.

If this was coded intending modification of original var z, I guess it should not have been shadowed, first of all. Or should be printed inside the scope.

Kind of equivalent to this:

var z: Int? = 42

switch z {
case var .some(z): z = 0 // warning variable 'z' was written to, but never read
case .none: break
}
print(z) // warning expression implicitly coerced from 'Int?' to 'Any'

I can see a potential confusing point in shadowing technique (Only if this was written in plain text editor without diagnoses), but not in this particular proposal.

And Xcode / sourcekitd is very kind:

var z: Int? = 42
if var z { // warning variable 'z' was written to, but never read
  z = 0
}
print(z) // warning expression implicitly coerced from 'Int?' to 'Any'
1 Like

+1

I’ve started reading about this syntax when it first was a pitch and I started typing a response to the pitch multiple times wanting to write something about clarity of language, being confused about several issues in different constellations and so on.

After reading through this discussion right here, most of the arguments I would have come forward with, have been brought up by others.

Now I’m at a point where I can safely say, that none of my arguments would have had any impact on the most common argument those approving this proposal have: this proposal eradicates boilerplate. Yes, we would have to get used to the new syntax because we just don’t know better for now. I presume new developers won’t learn the old syntax (if let x = x) where you simple shadow and unwrap as they probably won’t ever know that something like CGFloat ever existed.

As I see no mayor downside of this proposal there is no need to be against it, imho. So I think this can be a handy addition to the language. I’m looking forward to see a SwiftLint rule that just auto-corrects the issues, which should be pretty trivial to do.

8 Likes

Hope I won't repeat myself too much. Let's start with the title:

if let shorthand for shadowing an existing optional variable

  • "if/guard let/var" only? I'd like to see a shadowing shorthand for all cases where variables are shadowed! not necessarily in conjunction with if statement.
  • "shorthand" leaving existing "let someName = someName" intact? I believe it is worth deprecating the latter syntax to be able recognizing shadowing vs non shadowing cases immediately without visually scanning the strings to see if they are different.
  • I'd also like to make the shadowing intent explicit even when the right hand expression is not the name of the left hand variable. (let foo = 123 // is it shadowing? no idea :thinking:. need to look in the outer scope to see if "foo" defined or not... or perhaps it is an instance variable? or maybe in a superclass? or perhaps it is a global variable?) Or it was shadowing before but not anymore, as the outer variable renamed but the inner code still incorrectly assumes it is shadowing scenario?
  • "optional variables" only? I'd like this to work with any types.

I'm firmly with @xwu now and think that something like this addresses concerns raised before.

There is a problem with accepting this niche proposal - it closes the door for the alternatives, either mentioned or possible.

PS. "shadowing" if introduced would be akin to what "override" does for instance methods, or what a similar "implement" could have been for implementation of protocol requirements.

PPS. originally I was pro this feature and as many others here considered it either a good thing or at least a minor/harmless thing; then took a deeper look at the problem, read and participated in the above discussion and changed my mind.

PPPS. I've mentioned override above... that's an idea... we can recycle the existing keyword for something new:

override let x     // like old `let x = x`
override let x = 2*x
override let x = x // ❌ Error. remove "= x"
let x = x          // ❌ Error. override explicitly
let x = 123        // ❌ Error. override explicitly
1 Like

+1 after requesting it too

Not familiar with the implementation

2 Likes

What is your evaluation of the proposal?

Strong +1. I've been waiting/hoping for a change like this to Swift for a long time.

This is a small thing, but the if let x = x pattern is extremely common in Swift code, it's not some advanced feature that some developers use occasionally, it's something that everyone uses everywhere; and while sometimes it makes sense for the variable to be assigned to a different name for the non-optional, from my experience in most cases it tends to be the same repeated name. A small improvement multiplied by hundreds of files and by millions of Swift developers means a lot of keystrokes and a lot of code reading time saved in total. It also means the assignment statement (especially with multiple variables) will be more likely to fit into one line, making it more readable and giving less incentive to shorten variable names to something like svc or uitv just to make it fit into 120 characters (which is definitely something I've done once in a while).

This repetition in let something = something always felt wrong to me somehow, it's like you're assigning a variable to itself. You know you aren't, but somehow that's what it looks like.

Looking at the alternative approaches listed in the proposal (new keyword/symbol etc.), I agree that the selected one probably makes more sense. It does feel like it fits best with the existing usage of if let and similar statements. I was a bit on the fence about the if let x? variant, but the arguments against it in the proposal (consistency in case of multiple assigned variables) are also convincing.

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

I've read most of the proposal (scrolling through the parts about the proposed ref thing because I know nothing about that).

==

Edit: I'm not sure about the if var x part, it does seem more confusing. But then also if var x = x is also something I don't think I've ever seen anywhere or used myself, and yet it is technically possible to write, apparently.

8 Likes

+1. I personally do not find the new syntax confusing nor inconsistent. This feels like a logical evolution to me.

Yes. I'm used to if let thing = thing syntax and I don't personally find it offensive, although I agree with the sentiment that it's tedious (even with Xcode's autocomplete help). Without a doubt I would use this new syntax if adopted. I am especially excited to use this with guard statements, as I use those very frequently to verify preconditions in my code:

// take an optional to simplify usage at point-of-use
public func doWork(with argument: String?) { 
    guard let argument else { return } // SO NICE!!!
    // ... do the work with a non-nil argument
}

Yes, I think so. We've embraced the if let syntax in Swift, and this feels like the next logical step down that road, and even goes so far as improving that syntax without introducing new keywords or bizarre unwrapping spellings. I am unconvinced by the arguments that this will cause confusion amongst new developers, and am equally skeptical that this will be confusing to seasoned developers.

N/A

I've been following along on the threads and reading all the replies.

11 Likes

-1

No.

No. As stated in the Commonly Rejected Changes referring to this exact proposed change,

it is favoring terseness over clarity by introducing new magic syntactic sugar.

N/A

I followed the pitches, read the proposal and reviews upthread.


My detailed review is as follows:

There indeed exists some dissatisfaction towards the current syntax for optional binding, as evident from the large number of reviewers favoring a change to it. However, I would argue that the dissatisfaction around repetition is not well-founded, and that the proposal doesn't really solve any problem.

  1. In the proposal is this example that is meant to illustrate that repetition makes code difficult to read:

    let someLengthyVariableName: Foo? = ...
    let anotherImportantVariable: Bar? = ...
    
    if let someLengthyVariableName = someLengthyVariableName, let anotherImportantVariable = anotherImportantVariable {
        ...
    }
    

    In my opinion, this example is given in bad faith, because the difficulty is greatly exaggerated by cramming both optional bindings in the same line. If we rewrite the example like this:

    let someLengthyVariableName: Foo? = ...
    let anotherImportantVariable: Bar? = ...
    
    if let someLengthyVariableName = someLengthyVariableName, 
       let anotherImportantVariable = anotherImportantVariable {
        ...
    }
    

    then I believe many will find it much less difficult to read.

    With this new formatting, I would argue that any remaining difficulty is due to the long variable names instead of the repetition. Otherwise, if let x = x should be as much harder to read than if let x as if let aVeryLongVariableName = aVeryLongVariableName is than if let aVeryLongVariableName.

  2. The proposal argues that if let foo = foo is worth sugaring to if let foo because the pattern is common. However, such repetitive pattern is not unique to optional binding. Similar repetitions commonly occur in initializers and for-loops:

    struct S {
        let a: A
        let b: B
        let c: C
    
        init(a: A, b: B, c: C) {
            self.a = a
            self.b = b
            self.c = c
        }
    }
    
    let numbers = [1, 2, 3, 4, 5]
    for number in numbers {
        doSomething(with: number)
    }
    

    self.foo = foo and for foo in foos are common patterns that repeat variable names. The latter even has code-completion support in Xcode. However, even though they're common and contain repetitions, I doubt many considers it a good idea to sugar the above snippets like these:

    struct S {
        let a: A
        let b: B
        let c: C
    
        init(a: A, b: B, c: C) {
            self.a
            self.b
            self.c
        }
    }
    
    let numbers = [1, 2, 3, 4, 5]
    for number {
        doSomething(with: number)
    }
    

    Even though both sugars are easily teachable just like the proposed one, they are not beneficial to code readers, because as pointed out by @xwu upthread, these kind of repetitions are load-bearing.

  3. The proposed sugar makes optional binding more difficult for new Swift users.

    In addition to the point raised upthread that with this sugar, Swift users now need to learn yet another optional binding syntax, I'd like to point out that new Swift users will have to learn the existing syntax first in order to under stand the proposed sugar. This contradicts the principle of progressive disclosure of complexity. if let foo might make sense to an existing user with experience with the if let foo = foo pattern, but to a new user, it doesn't make sense, because there is no experience to base the understanding on.

  4. This sugar brings a regression to the discoverability of the shadowed variable.

    Currently, with if let foo = foo, IDEs such as Xcode allows you to jump to the definition/declaration site of the shadowed variable. If it's sugared to if let foo, then it completely relies on the reader themselves to search through the source code to find the shadowed variable. This can be difficult when the code base is large with many similarly named variables.

  5. The proposal points to the syntax for closure capture list as a precedent where a pattern "serves as both an evaluated expression and an identifier for the newly-defined [shadowing] variable”. However, this can only be a supportive argument iff it can be demonstrated that the syntax for closure capture list benefits from omitting the shadowed variable and that it's not more confusing than if the syntax allowed visible assignment from the shadowed variable. As far as I know, this has not been demonstrated to be the case, thus looking towards closure capture list's syntax is chasing consistency for consistency's sake.

8 Likes
  • What is your evaluation of the proposal?

+1 :+1: Simple; easy

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

Absolutely. It's a nice shorthand (and optional, too, so if you don't like it don't use it ;))

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

Yes.

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

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

I have read the study and followed much (not most) of the discussion.

2 Likes