Let's fix `if let` syntax

Personally I'm not fond of this approach. It is convenient, but I find it really weird that merely performing a comparison would change the type of a variable from optional to non-optional.

34 Likes

I think it's much less weird than if let foo = foo, but we're all used to that now.

9 Likes

I definitely think this is worth revisiting, despite the commonly rejected status. The key here is a) focusing on the refactoring/renaming aspect and b) increasing weather than decreasing clarity.

I think you make an excellent point on the refactoring front. Even more so the current syntax is certainly confusing to new comers to the language. Most new programmers struggle a bit with identically named variables in general. Most programming books will devote a section explaining the difference between the variable that you pass into a function vs the parameter that is used in that function with the same name (especially for languages that have mutable parameters by default). When I'm explaining Swift to someone I will often intentionally use different names when unwrapping an optional to make it clear that they are creating a new variable. Similarly I also see new Swift devs often add cluttered names like nonOptionalFoo to help them remember the difference.

I think adding syntax that is both clear and concise would be a big improvement to the language. if have foo reads fine to me, while if foo? is less clear about what is happening.

6 Likes

Again, the syntax doesn't matter here.

The problem is that this construct is creating code that's hard to maintain.

-ch

3 Likes

I agree. I made this point in my thread on Twitter but neglected to post it above.

The test for me is to read the code aloud (that's a trick that works for writing, in general).

"if let foo equals foo then ..." doesn't mean anything. There's no concept for a beginner to grasp onto.

Optionality is a hard thing to understand, and this syntax does nothing to make it clearer. One of the reasons I like "have" is that it's explicit, not that it's terse.

-ch

5 Likes

Speaking personally, not on behalf of the core team:

I agree this is something worth addressing. if let x = x { } is so incredibly common that it clearly deserves some form of privileged syntax. Personally I do like if x? { /* x is non-optional */ }, though I'm not good at spotting potential parse problems from adding sugar like this.

I agree that the clarity-over-brevity argument doesn't address tooling. But I also think there's a strong case just for the sake of clarity, irrespective of tooling. if let fooAutomationViewController = fooAutomationViewController { ... } is not clear. Swift removes verbose ceremony that can obscure clarity, and this clearly fits with that direction.

Regarding the commonly rejected list: people should take note of it, and the core team has added to it recently. But it needn't be treated as inviolable – we have also removed entries from it when sufficiently well-motivated proposals have come along (though note, the proposal that prompted that removal was rejected following review).

51 Likes

I tend to encounter the non-ergonomics of the existing syntax after calling a method that returns an optional, when I have stored the result to a temporary variable.

This is especially common for methods which take a closure parameter, since trailing closures are not permitted in the condition of an if let.

My preferred spelling would be either:

if unwrap x { ... }

or

if x? { ... }
9 Likes

The main reason if let foo = foo is clear to read is because we've learned what it does; in other words, it's probably not clear to anyone who doesn't already know Swift. You can read a lot of Swift code by sight and infer what it does if you have experience with other languages, but I'd wager most non-Swift programmers could not figure that out without it being learned (or without spending time breaking it down, understanding block scope and figuring out why both inner and outer scopes can be referenced, what the optional assignment operator does, etc).

That being said, "code readability by people who don't know the language" isn't necessarily a desirable goal, but if the argument is to make the code clearer and less confusing to read, it's only readable because we've learned how to read it. In an alternate universe where conditionally unwrapped optionals was written to be if @#$& foo { ... } for some reason, we'd understand what @#$& did after learning the language, but not because it was intuitive.

Any solution to this could still rely on requiring some learning (e.g. the if unwrap foo or if foo? solutions), and things would be no worse off than they are today. I'd assert that if foo != nil is intuitive for pretty much all programmers and is also the most descriptive of intent, but I'll take any improvement over the status quo.

21 Likes

I also agreed it should be improved through some form of syntax sugar, how about if ?x {...} ?

if x! and if !x have syntax clash with existing rules, if x? is ok but perhaps could incur recognizing confusion for new swifter. if ?can-I-unwrapped {} looks simple and natural, besides that I'm open to any other great ideas.

True, but we have intermediate sugar for that, on the way to if let:

if case let .some(value) = value { }
if case let value? = value { }
if let value = value { }

As such, if case value? { } * is good with me, because I see it as an extension of that pattern, but what do we do about this same issue when it comes to switch statements? Just a question mark?

switch value {
case ?: // same as…
case let value?: 
  • That would be shorthand for if case let value? { }, but as we also need if case var value? { }, I'm okay with leaving out the option for the shorthand above that doesn't use let.
2 Likes

Without dissuading this effort—indeed, I'd love if this is improved—I'll note some cases where if let foo = foo has effects beyond simply making foo non-optional:

  • foo is really self.foo; if we're in a mutating method or a class method, self.foo could change within the body of the if while foo stays the same
  • foo is a global variable (or type-level variable in a type-level method); the same could happen
  • foo is a captured var; the same could happen if foo is also captured in another closure. (This one's pretty rare.)
  • foo is a local var. With if let, this shadows the local foo, making it unassignable within the body.

One answer is to say that the shorthand will not allow any of these; it will only work when foo is already immutable (i.e. declared with let or as a function parameter). I think that's perfectly valid, at least for the first version of a proposal, since it's possible to open up later. I just want to make sure it gets mentioned.

37 Likes

I agree with you here, and I'd sharpen it to say that it's significant that an assignment is taking place (that is, a value is being moved or copied into a different variable).

It seems to me that eliding the fact of the assignment is a pretty bad idea, except that it's conveniently briefer — and that doesn't seem like a strong argument for change.

It also seems to me that a different kind of solution is already implicit in @chockenberry's original post:

  • Autocomplete after typing something like if let foo could be enhanced to suggest if let fooAutomationViewController = fooAutomationViewController when a matching optional variable exists in scope.

  • Refactor -> Rename could be enhanced to offer to change both the variable and its shadow. After all, this feature already suggests changing the name in comments too, though it leaves the final decision to the user.

17 Likes

This is a really great observation, Steve. A couple of observations:

  1. Optionality was one of the things I struggled with the most when first learning Swift: all the question marks and exclamation points didn't make sense. I had to learn the if-let construct in order to proceed.
  2. The mention that if-let is itself already a shortcut was news to me. We only learn as much as we need to get a job done.

In the case of learning, less is more.

-ch

5 Likes

While I agree that the duplication pain is real, this example feels like a straw man to me. If you name the constant controller instead of favc, the code becomes much more readable, and as long as you don’t change the type to something that isn’t a controller, renaming isn’t an issue.

11 Likes

There are surely cases where being generic in the outer scope can get around the issues, and in many cases, it's warranted.

But I tend to be a specific as possible when entering a block. Something that's named controller won't always be enough information. Is it a split view controller? A navigation controller? Or maybe a root view controller?

In cases where you're doing structural refactoring, a view controller can easily become a subclass, and you'll want to carry that information into the block.

-ch

3 Likes

That is the optional-binding-condition production in the grammar. There's a related situation where you also don't get completion, which is the case-condition production, which looks like this:

let space: SwiftUI.CoordinateSpace = ...

if case let .named(name) = space { ... }

// or, equivalently

if case .named(let name) = space { ... }

The problem is that you don't get completion when you need it:

let space: SwiftUI.CoordinateSpace = ...
if case .<esc>

The editor has no idea what to suggest because it doesn't yet know (when I press esc) that I intend to match space.

Note that switch doesn't have this problem because the initializer is given immediately after switch, before any of the patterns.

In both if situations, the editor could offer useful completions if the initializer came before the pattern.

Let's suppose be acts like = except the sides are swapped: the initializer goes on the left and the pattern goes on the right. (Yes, be is a terrible spelling.) Then:

let fooAutomationViewController: FooAutomationViewController? = ...

if fooAutomationViewController be let <esc>

When I press esc at the indicated position, the editor already knows the initializer is fooAutomationViewController, so it can suggest fooAutomationViewController as the new binding. And it can work for a more complex initializer, too:

if view.superview be let <esc>
                         // suggest `superview`
if view.subviews.last be let <esc>
                             // suggest `last`

And this also lets the editor make useful suggestions for case-conditions:

if space be case .<esc>
                     // suggest `global`, `local`, and `.named(_)`

Ideally, case-condition and optional-binding-condition would be commutative around the =, so we could just write

if fooAutomationViewController = let fooAutomationViewController { ... }

if space = case .global { ... }

without invalidating existing code, but I don't know how tractable that is.

7 Likes

For this line of improvements, check out the thread on is case and possible expansions: Proposal sanity check: assigning a case statement to a boolean - #13 by xAlien95

I think it's separate from wanting a very short and clear "unwrap-Optional-and-shadow" form, though.

7 Likes

I more-or-less hate all of these spellings. Simplest and clearest would be comparison to nil unwraps an optional in the following scope.

if myViewController != nil {
// it's unwrapped in here
}

There's no problem with naming/renaming of the variable. There's no oddball syntax or new keywords. I've wondered why this doesn't work since my first day with swift. What could be simpler?

9 Likes

About if value != nil, it is really problematic. Currently this code works.

var value: Int? = 42
// some process
if value != nil {
    // some process     
    // finally make value nil
    value = nil
}

If Swift introduces if value != nil that implicitly unwraps value, it starts causing error, because value inside if is no longer Int? but Int. It's source breaking change actually.

(EDIT)
About this smart cast, type guard, or 'inference', we did further discussion. If you have any objection about this, please check the link below.

27 Likes

I'm open to reconsidering this as well. I think we need clear syntax for this (introducing a context sensitive keyword like unwrap is a good idea), and it should work consistently for if and guard, and we should consider how it works with case patterns in for/in loops as well.

I am pretty opposed to overloading the if x? { syntax. There is no reason to overload ? to mean yet another thing. I'd recommend investigating the direction of a (context sensitive) keyword. It could even be a statement modifier, before the if/guard.

-Chris

41 Likes