Let's fix `if let` syntax

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.

36 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

40 Likes

The ditto mark (two apostrophes) is available.

if let '' = x {
  /*...*/
}

func f(_ x: Int) {
  var '' = x
  /*...*/
}

UPDATE: My current preference is two backticks, on the right-hand side.

if let x = `` {
  /*...*/
}

func f(_ x: Int) {
  var x = ``
  /*...*/
}
3 Likes

I totally agree with this. This proposal would definitely make for a cleaner experience when refactoring and maintaining code in the long run. I’ve often found myself losing my train of thought when reading over code that contains many of these if let statements just like the examples you showed.

1 Like

My feeling is the opposite. An additional word seems clunky to me. Why introduce a new word when ? is already used for the very similar case let x?: in switches – a use case that's less common in day-to-day code than the one we're talking about here but definitely benefits from a delightfully lightweight syntax.

And of course using a word means choosing a word, and they will all be unsatisfactory. unwrap would feel strange because optional unwraps are everywhere in Swift, yet this word would appear in only this one particular form.

(if have fooViewController skews a bit too lolcat for my liking)

29 Likes

I don't think the current syntax is that bad — but as there is surprisingly much support for yet another piece of sugar, I wonder why

if let fooViewController {

has not received more consideration.
Imho keeping the "let" is a big advantage, because it does not hide creation of a new value (with a different type), it also works for var — and it is compatible with the common name for the whole construct ("if assignement" / "if let block"...)

27 Likes

FWIW, I think if var x { ... } would suffer similar problem to var-parameter (SE-0003's removed syntax).

3 Likes

I think new syntax sugar has too limited usage.

Here, these codes should not work as unwrapping, because value is property of Mydata and its type must be Int?. It is strange if the type of property changed.

struct MyData {
    var value: Int? = nil   
}
let data = MyData(value: 10)
// you cannot use unwrapped value `data.value` 
if let data.value? { /* ... */ }
if have data.value { /* ... */ }
if data.value? { /* ... */ }
if data.value != nil { /* ... */ }
if let '' = data.value { /* ... */ }
5 Likes

Love the goal here.

Here’s a few alternative syntax ideas

let color : Color? = getFaveColor()

if exists color { }

if color exists { }
1 Like

Is if let? optionalVar {} an option? It reads pretty clearly to me, and it has precedent with try?.

2 Likes

More possible spellings

if nonnil value { }

or

if some value { }

(Oops, that one looks too much like opaque types. Probably unambiguous to the compiler, but confusing to the human.)

2 Likes

This is a great point. Searching through our codebase of some 100 KLOC Swift, if let thing = other.thing is actually much more common than if let thing = thing. If the goal here is to take away the pains of duplicated names, the solution really should work with optional chains too.

3 Likes