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

+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
  1. 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:

The examples you suggest are not the same pattern at all. The pattern is not "repetition" but rather the shadowing and unwrapping of an optional value.

Another strong +1 for this

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've been following this proposal and other similar ones for a while.

2 Likes

I didn't read that massive thread and I want to quickly ask if it has been explored if the unwrapped instance could instead be shadowed by a version with surrounding back ticks?

let foo: Int? = ...

if var `foo` {
  `foo` // <-- unwrapped
  foo   // <-- possibly the original optional value
}

However I'm not entirely sure if we can do something like this at all because of the current back-tick rules we already have. :thinking:

I know I already mentioned this upthread, but I think it is worth stressing it. Seems like most people that think this new syntax is not confusing have the benefit of having dealt with if let foo = foo before. New Swift users will not have that benefit.

I know that if let foo = foo is already foreign to most developers coming from other languages, but there is no need to exacerbate this, making the language inconsistent and "ad-hocky".

It is much better to introduce a new concept that works anywhere where shadowing may happen and can be explained on its own, without resorting to an existing, already rather unique, feature of Swift (if let). Again, the issue is shadowing, not unwrapping. There is no need to tangle both concepts if they can be easily explained separately.

I imagine some might dislike the addition of a new keyword that would be used a lot, but we already have to live with keyword "explosion" in some pretty common cases like the public keyword in entity/model definitions.

public keyword example
public struct Foo {
  public let foo: String
  public let bar: String
  public let baz: String
  public let bah: String
  public let meh: String
  public let muh: String
}

This is a bit verbose, yes, but it is essential.

3 Likes

Another point I would like to make is that the unwrap keyword some proposed does not actually fix the issue. It only duplicates the role of what the if keyword is already doing.

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.

Some further thoughts on this. if let x? can be seen as a two-feature: allowing optional some patterns outside of case patterns, and tolerating the sugar itself. In this sense if let x? looks more like it was meant to be if case let x? but became a condensed intermediate form between pattern matching if case let x? = x and optional binding if let x = x. I reckon individual sugared forms for each of these constructs is a better choice in the long run than a single special combined one, more so because unwrapping multiple levels of optionality simultaneously is often not a sensible thing to do — something that benefits from the emphasis the more verbose pattern matching provides.

Oh, I also forgot about the ? sigil some proposed. It is the same thing as the unwrap keyword. It signifies optionality, or unwrapping, which is already dealt with by the if keyword. We need syntax to signify shadowing. We're already covered for unwrapping.

3 Likes

Exploring the shadowing keyword idea a bit further:

var x = 123
var y: Int? = 123
var z: Int! = 123
do {
	override let x // type is Int
	override let y // type is Int?
	override let z // type is Int?
}
do {
	override let y = "123" // type is String
}
if override let y {
	// type is Int
}
if override let z {
	// type is Int
}
if override let z = optionalStringExpression {
	// type is String
}

BTW, we are just passed the specified review end date (I appreciate it is not set in stone and a few more days could be allowed). Anyway, are we closer to consensus than before? Looking forward to seeing the review outcome.

No, they're not the same pattern, but patterns that are similar in their use of repetition. Indeed the optional binding pattern is more than repetition, but what's proposed is not changing anything but removing the surface-level repetition. So, in my opinion, it is apt to compare the optional binding pattern with other repetitive patterns in the context of the proposal.

Moreover, recognizing that the optional binding pattern is more than its syntactical repetition points to a danger of the proposed sugar: It further removes the semantic information of shadowing and unwrapping from the reader's perspective. This goes against the language's "feel and direction" that effects should be clearly identifiable, which as far as I understand is the reasoning behind markers such as await and try.

2 Likes

Yes, I used the public keyword example above because it acts like a modifier for the following declaration, but a much better example would be the try and the await keywords, which are unnecessary, but are required because they make the code much easier to reason about. A shadowing, shadow, or any other nice keyword we find, would provide the same benefit.

Arguably, we do have sugar for this pattern -- synthesized memberwise initializers:

// No repetition!
struct S {
    let a: A
    let b: B
    let c: C
}

While there are multiple places where foo = foo repetition exists in the language, each instance needs to be considered separately in its own context. Techniques that work well in one context don't necessarily work well in others, like you pointed out. I don't think addressing this repetition in one common case (optional unwrapping) implies that we need to address it everywhere in the language.


I'd argue that it's the let foo that indicates shadowing, not the = foo. For example, this example still shadows without any repetition:

let foo: String? = "foo"
let bar: String? = "bar"

if let foo = bar {
  // foo has been shadowed, without any `foo = foo` repetition!
  print(foo) // prints "bar"
}

So I disagree that the shorthand syntax would make shadowing less clear (since the part of the syntax that indicates shadowing is still present).


While we could provide general purpose sugar for shadowed assignment, this doesn't seem like a particular common pattern that needs to be sugared. One data point for this is that I'm not aware of anyone asking for this sort of feature before, while sugar / shorthand for optional unwrapping has been requested time and time again over the years. Unwrapping an optional is a fundamental operation that is important / common enough to deserve special consideration.

4 Likes

For me, the proposed shorthand is very analogous to not requiring self before accessing members of a type. The main arguments for and against are very similar to this proposal.

One the one hand, requiring self is more explicit and therefore arguably more clear. The argument can be made that omitting self sacrifices clarity for the sake of brevity. Requiring self also disallows accidentally shadowing a member since self.foo and a local foo are not the same.

On the other hand, requiring self can be seen as extraneous boilerplate that adds noise and makes the code less clear. So it can be argued that the brevity enhances the clarity. This approach allows accidental shadowing to occur.

In the case of requiring self, the choice made was to accept the clarity that comes with brevity and self is not required, despite the potential shadowing issue.

So, I think the proposed shorthand is very much in line with the language’s “feel and direction”.

I think it is also worth noting that Swift has never required self for member access except within closures (and that requirement has been made less strict over time). The proposal to require self was in late 2015.

The ‘clarity through brevity’ of not requiring self has not seemed to be a significant point of confusion with developers since Swift’s inception even though it is arguably less clear than ‘clarity through explicitness’.

I believe this proposed shorthand is very much in the same vein.

8 Likes

Ideally what we'd want is a "ditto" mark. Unfortunately the only common way of writing one seems to be some form of '' (two vertical apostrophe-like lines).

For loops aren't usually exact repetitions (except for sheep in sheep { ... }) and could vary by language. Xcode actually does autocomplete singular based on a plural variable name, e.g. for go|ose in geese { ... } which is cool.

Wouldn't it be possible to teach the proposed sugar first, right after teaching how to make/get optional values -- then teach the = ... form as a more general extension? Currently the first thing TSPL teaches, right after introducing optionals, is checking for != nil and then force-unwrapping. Then it goes into optional unwrapping and uses the if let actualFoo = possibleFoo approach, which could be a bit misleading. Starting out with if let foo { ... } as a way of saying "if foo is not nil, recreate foo as non-optional" might be a smoother learning curve.

I think "Go to Declaration/Definition" would work pretty intuitively. "Go to References/Callers" on the other hand would be ambiguous, specifically if your cursor is on if let foo| and you want to go to the next reference -- the next inner unwrapped reference or the next after the block? Refactoring tools might need a checkbox to toggle how variables should be renamed, either renaming all instances or just within the current if let scope, for example. How realistic is that?

Re: explicit shadowing

I think shadowing naturally comes out of block scopes which are a universal concept in programming. Are there any other programming languages that have an explicit shadowing mechanism?

Specific questions:

  1. What about function parameter names. They could be shadowing outer variables. Would something like this be allowed or even enforced?
let foo = ...
...
func f(shadowing foo: ...) {
    ...
}
  1. What would be a good reason to shadow a variable within the same scope (not a nested scope)? Like:
let foo = ...
...
shadowing let foo = ...

This is not possible in Swift today, unless you count guard let. I'd love to hear what other things it could be useful for. But it'd probably be good to create a new thread for this.

Another potential for confusion that I haven't seen mentioned yet is this:

if foo { ... } // "if foo is true"
if let foo = foo { ... } // "if foo is not nil, bind to shadow-foo"

The proposed syntax could be confused for a hybrid similar to what I mentioned earlier:

if let foo { ... } // "if foo is not nil and foo is true, bind to shadow-foo"

Given that if foo { ... } is currently not allowed if foo is an optional Bool.

But I think this potential for confusion unlikely. Just bringing it up just in case.

I didn’t want to make this thread longer but I guess one more vote doesn’t harm.

  • What is your evaluation of the proposal?

-1. The work put on this proposal by the author is amazing and he’s been answering all concerns, kudos for that.
Ultimately no new argument has made me change my mind from the pitch thread and even before.
This is one of those changes that attracts a lot of people because is just sugar but that I believe it has a detrimental aspect to the language.

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

No, there is no problem to be addressed imo. The readability of the current code is way more clear than any change proposed.

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

No. One thing is to make things nice the other is to remove so much syntax that you get puzzled when reading the code.

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

The closest it gets is the flow sensitive nullability of other languages which also Swift doesn’t get into.

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

Been following all threads about the topic for many years. And I write code like this every day without being bothered by it.

Ultimately as with any sugar it touches directly into personal opinions so is hard to argue one way or another.

But I want to thank again the author for all the great work!

6 Likes

By following the lines of your very own logic above, unwrapping itself has nothing to do with shadowing, and we already have a well established mechanism for it:

if let foo = bar {
    // unwrapping, not shadowing (if foo was not defined outside)
}

It is remarkable to see how people here are so focused, even obsessed with the very niche (even if frequent) corner case:

if let foo = foo {
}

and can't see beyond that niche corner case.

To me it is not (just) about sugaring... it is way more than that. To see right away on the spot that this is shadowing without mentally scanning the two strings, one of which can be well above, even a few (dozens or hundred) lines, and figuring if they are different or the same is the whole new feature, not just a convenience to not type something twice. Similar to how by seeing the "override func" you immediately understand that this is an override, without the need to scan through the code base and check if the name and signature matches... And when refactoring happens and the outer name is changed outside - compiler would check straight away and warn you that what you think is an override is no longer, so you can correct the code. It is (to me) now obvious that this feature shall be supported not just in the particular corner case of if/guard. And, as I mentioned, initially I was thinking exactly like you, being pro this feature when entered this thread... Think deeper.

3 Likes

Being frequent meant this isn't niche, right? Unwrapping an optional without changing the name of its variable is a pretty frequent operation. There are several non-contrived examples of common patterns that do this in the proposal and review thread:

// From the proposal body
struct UserView: View {
  let name: String
  let emailAddress: String?

  var body: some View {
    VStack {
      Text(user.name)

      if let emailAddress = emailAddress {
        Text(emailAddress)
      }
    }
  }
}

I'm less familiar with common patterns involving shadowing in arbitrary blocks / scopes. The best example of this that I can think of is shadowing a method parameter to make it mutable, but this is pretty infrequent compared to optional unwrapping.

I find Ben's discussion above, about how this proposal relates to shadowing in general, particularly valuable:

6 Likes