`if let` shorthand

Please share the "if var" and "if let" stats you see in there, the link doesn't show results for those who are not already on that system and leads to a "Join waiting list" screen.

For God's sake, yes, yes, a thousand times yes. This change should have been made five years ago. So obvious. So frequently asked for. So much cleaner. So surprising that it doesn't already work this way.

1 Like

I know in the thread most of the discussion focus has been on if let, but I find I tend to use guard let much more often since I'm most often unwrapping as a precondition.

If folks are gathering stats on if let / if var it might be useful to also gather stats on guard let and guard var.

I wonder if guard var x = x usage might be higher than if var x = x usage because the mutable shadow variable is in scope for the entire top level of the block after the guard statement.

10 Likes

Unfortunately, while more accurate, the new code search isn't designed to offer the statistics we want. Searching across GitHub returns 100+ pages of results for both forms, but that doesn't really tell us anything. Going repo by repo is more accurate, so here's an example from Apollo:

if let: Instances in 53 files, didn't count.
if var: One use.
guard let: instances in 60 files, didn't count.
guard var: Three uses.

So var is not unused, but probably a magnitude or two less than the let version.

4 Likes

Shadowing with guard let is extremely common, but I still think that explicit shadowing is preferable to shorthand.

Perhaps I haven’t been paying sufficiently close attention, but I have yet to see an example of Swift code that would actually utilize this shorthand (meaning it is shadowing with an optional binding) and doesn’t suffer from readability issues better solved by method composition or preventing invalid state.

3 Likes

Which is not surprising given if let is one of the most common idioms in Swift. There is no actual meaning conveyed by the ratio of one versus the other, and even if it were two orders of magnitude it wouldn't make the case against the consistent application of this sugar for both let and var any stronger. The argument for inconsistency that would stand up is active harm of the var form. That case has been made here, but on the basis of hypothetical misunderstanding that rings hollow (and is not backed by evidence of the same harm occurring in the very similar existing if var x = x syntax).

6 Likes

I agree, I'm just offering better data and pointing out that GitHub's existing code search shouldn't be used by those who care about the ratio.

2 Likes

I analysed examples brought up by Ben. In more than half cases "if var" usage is unjustified. A typical example:

Ranchero-Software/NetNewsWire/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift

if var feedInFolder = feedInFolders[relationship.folderName] {
	feedInFolder.append(relationship)
	feedInFolders[relationship.folderName] = feedInFolder
} else {
	feedInFolders[relationship.folderName] = [relationship]
}

a better one line equivalent would be:

feedInFolders[relationship.folderName, default: []].append(relationship)

In the rest of the cases it would be trivial to introduce a variable "var x = y", if we didn't have "if var x = y" feature today.

In none of the examples I saw shadowing ("if var x = x")

It looks like we are polishing a feature no one is using!

This is a stylistic preference (albeit one I also prefer) and not, as you are making out, some kind of mistake on the part of the author. Many uses of if let could be replaced by map or ??, and many for loops by flatMap... sometimes for better, sometimes worse. One person's neat one liner is another's too-clever obfuscation.

In the rest of the cases it would be trivial to introduce a variable "var x = y", if we didn't have "if var x = y" feature today.

Yes, that is the description of what if var does.

Again, to make a case for deliberate language inconsistency, you need to point to harm, not just express personal taste preferences or claim infrequent use.

7 Likes

I feel this is sufficient justification to not make if let potentially more obsfucated by adding implicit shadowing.

4 Likes

I'm not sure it's fair to call the shadowing "implicit". There are some parts of the language which are truly implicit - for example, simple enums without payloads automatically conform to Equatable, even though you never write that conformance anywhere:

enum Test {
  case a, b, c
}
func doSomething<T: Equatable>(_: T) { /* ... */ }

doSomething(Test.a) // Works!

Now, [!!!] I don't want to divert this thread [!!!] with a discussion about whether this is a good thing or not. I'm only bringing it up to say that this is an example of something which deserves to be called "implicit". The shadowing introduced by an if let shorthand is not like that feature; it is very much explicit. You have to actually write it in code for any shadowing to occur.

Of course, whether a developer understands that shadowing is happening depends on whether they understand the language construct, but that is true of every other language construct as well.

13 Likes

Would your concerns about mutable re-binding be solved by a syntax that doesn’t use a keyword at all? For example, if foo? { } vaguely resembles other optional-related syntax like as? and ?. without inviting questions about what it means to change mutability. It also “feels like” the equivalent Kotlin feature, without the extra magic of silently dropping the optionality within the if branch.

Right - this is like a (very sad) inside joke among Swift programmers. It is true that we currently force all swift programmers to learn this, but that doesn't make it the right thing!

I don't see your point here Ben. I don't see a credible claim that if var and if let occur with the same frequency. Neither you nor anyone else has made that claim.

My claim is that we should look at the user problem that needs to be solved, and figure out the best way to solve it. Yes, just sugaring if let x { and if var x { would make people "stop complaining", but that doesn't mean it is the best solution.

The actual problem here is the if let x = x { problem, not the var equivalent. Unwrapping optionals with a shadowing rebind needs to be solved (in my opinion) and I expect that we will also have to solve the same problem with borrowing rebinds. The mutating version of this is not at all important from the data I've seen, so claims we need to build a "superficially general" solution doesn't seem justified. There seems like ample space to explore better approaches.

You seem to be hilariously ignoring the point of my text you are quoting. The text you are quoting is intentionally trying to distill the difference between whether consistency with var/let is important or whether solving the "if let x = x {" specific problem is important.

I agree the later is important, but there is no evidence the former is. While I am often on the side of consistency, I am not for surface level consistency where it doesn't make sense.


If you back out of this and look at how Swift works, it is worthwhile to remember that the existing if let syntax is a bad abuse of pattern matching syntax. if let x is not specific to x being a name, it is a series of special cases around a more general pattern matching and desugaring syntax that was over priviledged for optionals. This overpriviledge forced us to further complicate the language by introducing if case (and guard, etc)

The more principled Swift 1.0 design would have been to use if let x? = expression to unwrap optionals (Rust learned from this mistake and corrected it). If we would have done this, we would have decided it wasn't ergonomic enough for the common case, and while therefore have originally introduced the "unwrap" syntax for the optional case (if unwrap x = expression), instead of overloading keywords like we did.

This would have made if let x? = expr { the general case (eliminating the need for if case entirely). The if unwrap x = expr { sugar would have covered the common case, and then if unwrap x { would be the privileged form of if unwrap x = x {.

Given where we are now, I don't see any reason not to fix this. I also don't see any reason to see this as an overreach. There are no compatibility problems with this - correcting this now would make Swift more uniform and teachable AFAICT.

Like some others on this thread, you are stating your opinions as though they are facts without justifying them or acknowledging other people's perspectives and the truth behind them. There is no hyperbole to claiming that "eliminating some of the syntax" makes it easier to overlook things -- it seems plainly true that less syntactically heavy things are easier to overlook than syntactically heavier things.

The actual point here is that people will see mutations of myThing later in the code (myThing = thing()) and assume the original is updated. Because .... that is how swift works. Yes, rebindings and shadowings of the same name are possible, but we shouldn't make them prone to introducing new bugs with minimal syntax!

I completely disagree. We are introducing new type system features for ownership. You can't pretend they don't exist. We have survived for 7 years without this. You can't pretend that isn't the case.

How would you defend rushing a decision about this when ownership is close at hand?

-Chris

13 Likes

This is not true in general; heavy syntax isn't the same as clearly expressing what you mean.

For example, completion handlers are syntactically heavier than async-await; yet it is precisely that heavy syntax which makes it difficult to determine at a glance that events are processed at the correct point in an asynchronous pipeline.

Heavier names can also be less clear than shorter names (hello, NSPersistentStoreCoordinatorStoresWillChangeNotification vs NSPersistentStoreCoordinatorStoresDidChangeNotification).

Not saying that your other points don't have merit (although I don't happen to share them), but the idea that "less syntactically heavy things are easier to overlook than syntactically heavier things" is not "plainly true".

5 Likes

I have never liked the if let x = foo() syntax, since it doesn’t look like a conditional check.

That said, I would love a good syntax to avoid sprinkling ! all over.

My suggestion is something like this:

If x != nil { x! in
    foo(x)
    bar(x)
 }
1 Like

I never saw anyone else is doing this, but that doesn't stop me from using this pattern in C++ as well!

#define let const auto
#define var auto

if (let p = pointerExpression()) {
     p->foo(); // safe
}
for (var i = 0; i < 10; i++) {
}

How could that look with explicit type annotation?

if let x? : Int = genericFunction() { // or Int?
}

how that (if case) would look?

Hypothetically if we found a better alternative would it be possible deprecating "if let/var x = y" (with shadowing or not) or is it too late at this stage?

That’s a fair point. I suppose my actual complaint is that the proposed syntax is easily overlooked, specifically in the (overwhelmingly common) event that the block with an optional binding is followed by an else block.

Until they are actually accepted, they don’t exist. It’s almost inconceivable that they would be rejected, but that is technically a possibility.

I am reminded of the modern shift in web browsers to shorten displayed URLs to their subdomain, specifically to avoid phishing attacks. Sometimes more data can convey less meaning.

I disagree with this interpretation: completion handlers are inferior because they have fewer rules. In fact, I’d argue that one of Swift’s defining strengths is being able to write code under increasingly specific constraints, thereby allowing both users and the compiler itself to make more aggressive optimizations.

The vast majority of issues can be reduced to either allowing invalid state (e.g. non-optional optionals) or not having a language mechanism to make stricter guarantees (e.g. nonescaping parameters, compile-time evaluation).

I think forced unwrapping gets an undeserved reputation due to frequent misuse. It should be used if an optional might be encountered at runtime[1], but a code path for more gracefully handling a nil value is not written[2]. The only issue is when people learning the language allow optionals to propagate, then attempt to compensate by force-unwrapping everything at the point of use.


  1. If the possibility of a nil value can actually be ruled out, one should use the assertion form: unsafelyUnwrapped. The postfix ! is the fatal error form. ↩︎

  2. Forced try fills a similar niche: if you can’t rule out the possibility of a thrown error, but aren’t willing to gracefully handle one, try! is the best approach. In both cases, one may think of ! as an unimplemented (and most importantly unmistakable) stub for handling certain valid outcomes. Judicious use can massively simplify code while maintaining safety. ↩︎

Open question: imagine we didn't have the existing if let x = x sugar today — and were thus forced to use if case let x? = x instead — then would we want if let x = x?

My take is we're so used to if let x = y being sugar for if case let x? = y — itself sugar for if case .some(let x) = y — that most of us take that syntax for granted and even think of it as a basic construct rather than sugar for something more verbose. But we got used to it to the point that it became usual and clear to everyone what it does and we feel it very clear of what it does.

I think if let x will become the same natural syntax sugar for if let x = x as if let x = x has been natural syntax sugar for if case let x? = x for so long.


My personal opinion:

  • Allow if let x { as sugar for if let x = x
  • Allow if var x { as sugar for if var x = x
  • Don't allow the sugar on nested properties (ie if let x.y { would produce a compiler error)
4 Likes

That's a commendable attitude, and I've seen them quite a lot recently for some reason. Unfortunately, it's never that simple. Swift has never been in a feature rush*, where the first feature gets all the says, and everything else follows.

We have manifestos, guiding proposals forward and ensuring they fit together. The proposal template also has the Future Direction to see how it could further expand. The review template even asks whether it fits with the feel and direction of Swift. We can hardly view any proposal in isolation.

That is not to say that we should anticipate all possible variations of proposals yet to be thought of. Of course not, but it bears to keep in mind for directions that are extremely likely, especially one(s) that could clash with the proposal. It's not wise to dismiss that something because it has not been proposed. You'll also have a much easier time convincing people if you can show that the thing being brought up is either a) not extremely likely to happen, or b) not in direct conflict with the proposal, which is usually an easy bar to clear (not to comment on the current pitch and the new ownership model specifically).

* There are, of course, times when that is debatable or even controversial, but it's controversial precisely because that's not how we usually do things or what we perceive to be the right thing to do.

7 Likes

Allow me to clarify what I meant: the features don’t exist yet. The proposals do.