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

Yes, this discussion is a big waste of energy — but this time, something is different:
There is an implementation.

If this was about wether someone should spend time on coding some more syntactic sugar, I would definitely vote against it; but @cal already did that, I think it is quite terrible to discard that work light-hearted.
We saw requests for years, and I don't think there is another way to shut down this discussion once and for all — or would you (anyone voting -1) really request to remove the shorthand if we had it already, and go through the proposal process?

2 Likes

The rationale from the common rejections list for rejecting if let foo doesn’t strike me as particularly “factual”:

An alternative syntax (such as if let foo? { ... } or if let foo=? { ... } ) […] is often proposed and rejected because it is favoring terseness over clarity by introducing new magic syntactic sugar.

The original pitch thread began by linking the most recent prior discussion and noting that the reception there had been fairly warm, notably more so than even earlier discussions. That seems like sufficient reason to reopen the door given that the original rejection rationale is quite opinion-based.

Moreover, I believe the proposal answers the charge from the common rejections list pretty directly—the assertion is that removing the duplication is both terser and clearer. Of course, it is also perfectly reasonable to disagree with this assertion, and for community members to have their opinions unchanged from prior discussions.

That this proposal is up for review at all serves as a pretty strong signal that, at least in the view of the core team, there is enough reason to reconsider this common rejection presently (which seems like something they are well-positioned to determine).

These are really good points and I don't want to appear dismissive by not addressing them. I have plenty more to say on this topic, but as I wrote things out it felt like I was veering more and more off-topic for the review thread. I'd be happy to continue this thread of the conversation elsewhere if anyone feels like there's more worth discussing.

12 Likes

Here's a non patchwork solution:

let shadow x             <--->   let x = x
var shadow x             <--->   var x = x
if let shadow x {}       <--->   if let x = x {}
if var shadow x {}       <--->   if var x = x {}
do { let shadow x }      <--->   do { let x = x }
do { var shadow x }      <--->   do { var x = x }
func foo(var x: Int) {}  <--->   func foo(x: Int) { var x = x }

Would I prefer that myself? Not necessarily (too much noise that obscures semantics for me giving too little in return). But at least it covers all shadowing cases systematically. And it reads almost as English phrase: "let's shadow x".

PS. "shadow var x" also worth considering, would be inline with "lazy var x ..." / "weak var x ..."

shadow let x             <--->   let x = x
shadow var x             <--->   var x = x
if shadow let x {}       <--->   if let x = x {}
if shadow var x {}       <--->   if var x = x {}
do { shadow let x }      <--->   do { let x = x }
do { shadow var x }      <--->   do { var x = x }
func foo(var x: Int) {}  <--->   func foo(x: Int) { var x = x }
2 Likes

I also considered the approach you propose, but preferred to use the keyword as a prefix, instead of a postfix, because it acts like a modifier, and most (if not all) modifiers in Swift are prefix keywords, like mutating func, nonmutating set, final class, etc.

I don't agree that the proposal, as is, is clearer. I would argue that it makes the language more confusing and inconsistent. Take the following example:

let foo: String // The bare `let foo` here is a declaration with no assignment.

// Elsewhere in the code

if let foo: String { // The bare `let foo` here is a shadowing unwrap
  ...
}

The only thing different between these two pieces of syntax is the if keyword, but the if keyword pertains to the unwrapping part, not the shadowing part. There's nothing that helps me understand that the second piece of syntax is about shadowing. We're trying to fix the repetition inherent to shadowing by clinging to our collective memory of the annoyance of having to type the same thing again and again in this context of unwrapping. New Swift users do not have that baggage and if this proposal is accepted as is, they will have much less chance to absorb this pain. How is that going to make reasonable sense then? They will encounter that code and to explain it you have to write the full syntax and then rant about how that used to be super annoying.

The issue here is the ad hoc solution to shadowing unwraps and not shadowing in general. Shadowing is to blame, not unwrapping. Compare to the broader solution of the shadowing declaration modifier.

shadowing let foo: String // We're shadowing foo here.

// Elsewhere in the code

if shadowing let foo: String { // We're shadowing foo (because of the `shadowing` keyword) and unwrapping here (because of the `if` keyword).
  ...
}

The same syntax means the same thing anywhere it is used. It is consistent and it doesn't clash with an existing syntax. To me, this is a clear sign that the current proposal is not optimal.

5 Likes

Quick question: Would you agree that the current if let has the exact same problem? Imo the = foo part is no indication that shadowing happens — I'd say that overall, the nil-check is something you just have to learn, even without the proposed addition.

1 Like

You are right that the = foo in...

if let foo = foo {
  ...
}

...does not convey shadowing. It is the foo = that indicates shadowing due to the fact that it uses the same symbol as the one on the right hand side. That's the definition of shadowing. The current proposal aims to remove the one part that indicates a shadowing is going on without replacing it with something else to mark that we want to shadow without having to repeat ourselves.

3 Likes

Doesn't shadowing mean that you have a variable with the same name as another in an outer scope, not that it necessarily is initialized to the same value / copied? I think let foo when there is an outer foo is sufficient to show that shadowing is happening. In other words, I think the = is not an important signifier for shadowing.

2 Likes

Yeah, I get what you mean, it is not required, but then it is not clear that shadowing is taking place at the point of use.

1 Like

Overall -1 in it's current state. I don't think that the proposed change's brevity is worth the lost clarity.

I think that some of the alternatives presented seem interesting, and could possibly increase the clarity of what is happening (although they have their own tradeoffs).

I think that the annoyance of repeating long variable names is worth solving. However I think the solution need to maintain or increase the clarity of the shadowing that is occurring.

No, I believe it is choosing brevity or clarity.

N/A

I have read some of the past discussions, and followed all of the recent discussions.

2 Likes

Good point. Could be this then:

1) shadowing let x = y     <--->  let x = y // x was defined in an outer block
2) shadowing let x         <--->  let x = x
3) shadowing let x = 2*x   <--->  let x = 2*x
4) let x = x                      // ❌ error: use explicit shadowing
5) let x = 2*x                    // ❌ error: add explicit shadowing
6) shadowing let x = x            // ❌ error or warning: remove "= x"
7) let yourSurname = yourSurname  // ❌ error: use explicit shadowing
8) let yоurSurname = yourSurname  // ok (homograph spoofing "о" vs "o")

ditto for if / guard and var.

provided (4) & (7) will get out of favour in the future due to warning / error, the homograph spoofing attacks would be much more obvious (the "let x = y" pattern would be reserved for cases when x and y are different).

Edit: changed "shadow" to "shadowing" to be inline with weak/mutating/etc.

2 Likes

That line of logic I actually like.

2 Likes

-1

As someone who left the programming world and came back after a 15 year sidetrack into other fields, the absolutely most beneficial change I’ve seen has been the IDE support and changes to the culture of programming that allows for verbose, crystal-clear variable naming. Clear naming is the heart of readable code that’s easy to reason over. So I’m very sympathetic to the argument that syntax (like repeating the variable name in shadowing) that disincentivizes verbose naming is a problem.

I’m also pretty sympathetic to the argument that duplicating long variable names reduces readability.

With that said, I think this syntax is too terse and something is lost. I would prefer a keyword for clarity.

I’ve also been reflecting on the let vs. borrow question. It just seems poor timing to launch a new syntactic idiom for unwrapping just as we’re beginning to make progress on the ownership manifesto. You don’t have to squint hard to imagine a future where the right way to do an unwrap is a borrow rather than a let.

5 Likes

I think I get what you mean, because the only difference between this invalid code:

let foo = Int?.none

let foo

and this valid code as proposed:

let foo = Int?.none

if let foo {
    ...
}

is the tiny if keyword and the following block. Depending on what else is going on in the vicinity (other conditions, unusual formatting), one could be mistaken for the other.

However, I'm not convinced that this is worse than the existing (Swift 5.6) case:

let foo = Int?.none

let foo = foo     // invalid redeclaration
let foo = Int?.none

if let foo = foo {
    ...
}

which also differs only in the presence of the if keyword and following block. Maybe it's a visual thing that I'm not feeling, but at least on the basis of syntax, it seems to me that there is no difference in the difference, and that the non-obvious aspect is the optional unwrapping, just as it is today.

Also, the potential for confusion here is between a valid code and an invalid one, and it seems to me that being able to distinguish the two is not super important when working on the code because you will be surely notified if it's invalid. Even considering Swift's bias toward code maintenance, I would assume (because I'm not speaking from experience, so correct me if necessary) that generally code checked out from a repository will be valid, and becomes invalid only during editing, which assumes that you already understand that code.

(Or is there some other potential for confusion that I'm missing?)

So I get the impression that the real issue with the proposed sugar is in those situations where you're teaching someone or you're maintaining Swift code but don't know Swift that well. I can picture someone being confused by if let foo and moreso than by if let foo = foo. However, neither is very clear (re: optional unwrapping), and I wonder how hard is it really to make the leap: "if let foo... this is a variable declaration, but inside an if-condition... there's no type, no initializer... oh, foo already exists up here... ... ... foo is optional -- AH!". And you could always just google "swift if let".

Yes, it isn't super obvious syntax, but not everything is, even in Swift (ternary operator anyone?). Some things are just widespread convention, and arguably this sort of syntax could become a new convention that everybody just knows / is able to identify even within other programming languages with slightly different syntax / rules (unless the Kotlin model wins out).

Many reviewers here are arguing that it's too unclear, yet I suspect everyone fully grasped the sugar pretty quickly, so it's not necessarily first-hand feedback from the group who would really be affected by its supposed downsides, which makes me skeptical. However, it is a very important consideration and I obviously lack the knowledge, so I would defer to better judgment despite what I just said.

I'm mildly sympathetic toward this new keyword idea, though I'm afraid to discuss it any further in this thread. :face_with_peeking_eye:

2 Likes

What is your evaluation of the proposal?

A slight upvote +0.5 for if let x? {.
And a strong downvote -1 for if let x {.

I believe the proposed if let x { is counterintuitive, as you don’t declare a totally new uninitialised variable as let x keyword would suggest, neither you check for successful creation of a variable, what if let x could suggest.

It’s not like the omission of = x from if let x = x makes a huge difference to the file size or speed of writing the code. But I believe, because of the lack of these few characters, the code starts to be less clear to the novice reader.

I think the second option, if let x?, is far better. It visibly points out that x is an optional, which we try to unwrap and is more concise with the current Swift behaviour of case keywords.

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

I don’t think the omission of a few characters which can be easily just copy & pasted warrants a change to Swift. When it comes to mental overhead of verifying if these characters match, I’ve never experienced it, but I guess it may be enough to introduce this change, if people find it helpful.

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

I think if let x? fits well with the feel of Swift, however I’m not sure if let x works well with the current state of the Swift language.

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

I’ve read the initial proposal and some notable arguments in this thread.

2 Likes

I'm slowly gravitating towards this and coming to a conclusion that this is superior to the "if let" shortcut being discussed on the following fronts:

  • handles situation more generally, including cases outside of if / guard
  • makes shadowing intent explicit (prohibits the current implicit shadowing "let x = x" in all contexts).
  • prohibits repeating the same name (shadowing let someName = someName) thus freeing user's brainpower from parsing the string and checking if the two names match or don't match.
  • as a result makes homograph spoofing more obvious (a minor bonus point)

I appreciate that the cost of adding a new keyword is high, but in this case I'd consider doing this.

With that reasoning in mind I sadly have to change my +1 to -1 for the proposal being discussed.

1 Like

I found this to be a very interesting point. Reading through those who have said they found the proposed syntax lacks clarity, I don't remember seeing any examples of what the proposed shorthand would be confused with.

I think the notion of an unwrap keyword, or requiring a sigil like ?, stems from the fact that the existing if let foo = bar or the shadowing version if let foo = foo is not immediately clear for a newcomer as there is no indication in the syntax that unwrapping is occurring. So the existing optional binding syntax could very easily be said to be unclear when first encountered.

That said, the if let foo = bar syntax has been in place since the initial introduction of Swift and the idiom is taught to new Swift developers along with optionals. (I still remember watching the initial WWDC session that walked through the basics of the language and thinking, "oh I guess that's how you do that" when if let foo = bar was introduced.)

Despite the syntax having no indication that unwrapping is occurring, it is widely used and understood. So, I think the preference for a new keyword or a sigil stems from lack of obvious meaning of the existing optional binding syntax when encountered for the first time, although in practice the syntax is used and understood without confusion. I think the same thing will be true with the proposed shorthand.

Going through an introductory Swift book or course, typically the first time you ever see if let foo = bar syntax, you are told exactly what it means. And I would imagine the same would be true for if let foo.

I am curious, for those who feel it is unclear, knowing how if let foo = foo works, what other things would you think if let foo does? And once you learn what if let foo does, what other things would you continually confuse it with?

12 Likes

The "if let foo = bar {}" or the shadowing version "if let foo = foo {}" is totally clear and easy to remember. I even used that pattern in C++:

if (P* p = bar) { p->x = 0; } // older C++
if (const auto p = bar()) { p->x = 0; } // newer C++
if (let p = bar()) { p->x = 0; } // with my "var" and "let" defined

I totally appreciate how we may do the minor patchwork and go from "if let foo = foo {}" to "if let foo {}" - and it would be somewhat better than what we have now the same way as the road with holes and cracks patched is better than unpatched.

Having said that, I believe that we can do even better without focusing on the "if" and the "unwrap" aspects - there is more to it, as my message just above shows. i.e. the new asphalt for the whole road vs the patchwork here and there.

This is repeating what I wrote above, but the confusion comes from trying to reason what foo is.

In

func maybeUpdate(bar: Bar?) {
    if let foo = bar {
        foo.update()
    }
}

It's clear what foo and bar in the if let foo = bar { line are. Not only that but although this construct is particular to optionals, declaring a variable outside a closure whose scope is limited to the closure is used elsewhere, such as in for foo in arrayOfFoo { }.

Then if let foo = foo { is just the shadowing version of the above. It is perhaps less clear because the symbol foo is repeated but this a choice the programmer makes, and it's still easy to distinguish between the non-optional and optional foo once you are familiar with the pattern. Xcode too can tell them apart, if you need to highlight or refactor one or the other.

But in if let foo { what is foo? The optional? The non-optional? What happens if you 'Jump to defintion' of the variable inside the closure, or refactor it? If it's syntactically identical to if let foo = foo { then refactoring should work the same, but is can't as it can't now rename just one of the two foo in that pattern.

3 Likes

Since the existing if let foo = bar syntax for optional unwrapping is already heavily used and understood and newcomers seem to pick it up easily, introducing an unwrap syntax to do the same thing tries to solve a problem that doesn't exist.

To use your analogy, there’s a pothole in the existing road and if let foo aims to be a targeted change to fix the pothole.

There's no need to rip out the entire road and replace it with a new one to fix a pothole.

And unless you introduce a major source-breaking change, if let foo = bar syntax would continue to exist, so adding an unwrap way of optional binding would really be adding an entirely new road that runs alongside the existing one—both needing to be learned, navigated and maintained, with some people preferring one road and some preferring the other.

8 Likes