Let's fix `if let` syntax

Well, I don’t think of it as any one keyword that does the unwrapping. Isn’t

let x = x

basically just syntactic sugar for the pattern-matching construction

if case .some(let x) = x

? (I believe someone else made this point upthread.)

That’s why if let x? works for me: I imagine it as using the bare let (without a right-hand assignment) in its spirit of pattern-matching+binding, where the x? is matched against the optional x, the ? matches Optional.some, and so x itself is bound to the .some’s associated value aka the nonoptional value.

This may make more sense in my head than it does under rigorous examination. But I wonder if there’s possibility for a more general ‘unwrapping’ syntax that could include e.g. if let Wrapper(x) as well, where x then becomes the wrapped type within the if-block’s scope.

My impression for postfix ? is, that if I were to learn Swift from now on, it would be confusing maybe.
Because on the other places it will be treated as optional (optional chaining).

struct Model {
    let value: Int
}

var model: Model?
let optionalValue: Int? = model?.value // This is optional value
if let optionalValue? { ... } // This is not optional value
3 Likes

Ok but in current syntax if let x = x, 3 things are happening, the optional x is being unwrapped (in the current syntax as I see it, this is implied/hidden behavior, meaning there is no symbol that specifically does the unwrapping, but rather it is implied with the if), then the unwrapped value is being assigned to let x (using the = operator), and then, if non-nil, the scope is entered (as indicated by the if).

So, if we replace if let x = x with if let x?, we now have an explicit unwrap (the ?), implied assignment (there is no =), and an explicit if to enter the scope or not. So what I am saying is, with the explicit let x?, is the if actually still necessary?

1 Like

I really like if let shadow x as well, and it seems to be in danger of getting lost in the subsequent discussion. Explicit shadowing addresses the refactoring issue that Craig mentioned at the start, and I like that it's applicable to other shadowing cases and not just for unwrapping.

I have a harder time with '' because it's hard not to read it as an empty string.

3 Likes

I think the following is more accurate:

  1. the RHS is evaluated.
  2. if not nil, it is assigned to the LHS.
  3. if an assignment took place, the if block is entered.
  4. if no assignment took place, the else block (if present) is entered.
1 Like

That's why I propose using if let/var ?x instead of x?, prefix ? means unwrap and shadow operator :blush:

Oh, it's certainly confusing, and I don't think it's limited to "learners". if let strips it, but the same name-reassignment leaves optionality in place, in a switch (case let) statement. :persevere:

let value = Bool?.none

switch value {
case let value?:
  value // Bool
case let value:
  value // Bool?
}

I said above that if case let value? would work for me, because it matches what's already in the language elsewhere, as shown in this snippet. But having the ? remove optionality doesn't actually seem right/intuitive/consistent to me (is there a thread on why that functionality was chosen?), so perhaps we shouldn't let that spread elsewhere.

Whatever happens outside of switch cases should match what's in switch cases. If we're not going to improve switch statements along with whatever else may come of this, then I don't think it's worth doing.

2 Likes

Agreed, and without a keyword that can dig into an associated value for a specific case, I don't know how that could play out in a switch statement.

e.g.

@frozen public enum Result<Success, Failure: Error> {
  @worthy case success(Success)
  case failure(Failure)
@frozen public enum Optional<Wrapped>: ExpressibleByNilLiteral {
  case none
  @worthy case some(Wrapped)
let value: Int? = 1

if worthy value {
  value // 1 as Int

switch value {
case worthy:
  value // 1 as Int
let result: Result<Int, Never> = .success(1)

if worthy result {
  result // 1 as Int

switch result:
case worthy:
  result // 1 as Int
1 Like

Couldn't we just add autocompletion for if let? Rule: After typing if let + space, you get autocompletion for all optionals in scope. Also, refactorization could by default include variables bound by if let, if they match the name of the bound optional. Same would apply for guard let. Ideally also for if case: after typing the space, you get autocompletion for the cases of all enum values in scope, where the bound variables take the name of the associated value labels (like in switch's fix).

4 Likes

Have we considered taking this opportunity to actually add additional functionality instead of just sugaring?

It could allow us to refer to the actual original variable instead of shadowing...

var x:Int? = 1
if unwrap x {
    x += 1;
}
print(x) // 2

I think my preferred spelling of this would be to use is, which creates this functionality beyond optionals as well

protocol P {
    var value:Int {get set}
}

struct S:P {
   var value:Int
   var other:Bool = false
}

let x:P = S(value: 1)
if x is S {
    x.value += 1
    x.other = true
}
print(x) //{value: 2, other: true}

For optionals, you could either use the non-optional type name or nonnil

var x:Int? = 1
if x is nonnil { // if x is Int would also work
    x += 1;
}
print(x) // 2
2 Likes

This is nice, but wouldn’t it conflict with the existing is?

1 Like

This is spelled x != nil:

var x: Int? = 1
if x != nil {
    x! += 1
}
print(x)

The question of path-dependent typing is explored elsewhere in these forums.

It wouldn't work for computed variables, properties, expressions, global variables, static variables, property wrappers... Basically the only thing it would work with are function parameters. Compare that to the current if let that can unwrap basically everything.

1 Like

Just my two cents in this long discussion. I really like the have keyword that @chockenberry suggested. But I'd like it to be modified to have the ability to specify var semantics when necessary:

// The common case...
if have something {
  // something is a `let` here...
}

// Explicitly specify `var`...
if have var something {
  // something is a `var` here...
}

// Or explicitly specify `let`, which is the same as omitting it...
if have let something {
  // something is a `let` here...
}

After reading this thread over and over again for the last few days, I agree that if have x {} or if let x {} is the best syntax.

if x? {} is a big no for me, because it interferes with the expected boolean logic and is not clear. It is the same ambiguity happens in JavaScript.

if let x {} is logically the best choice because it matches the current if let x = x {} For this reason alone, I would chose this as the accepted pattern. It also makes it easier to rewrite as an if let x = y statement if later needed and suggest the existence of having an if var x statement as well.

but if have x {} makes the most linguistic sense, so I understand why people might prefer it; however, it is clear that we are forgetting that if var is a thing as well, and thus if have would not allow us to have the power of if var. Therefore if have should not be chosen (which does make me sad, I quite liked it).

I am concerned with what would happen under if let/have x?.y?.z {} what would the generated variable be called? And should we explicitly not allow this construction? Should we always name it the last caller spelling, e.g. z and therefore if let x?.y?.z -> if let z = x?.y?.z

Also, I know why we need this, but I have no care in the world that I have to type if let x = asdfads?.asdfadsf?.asdfasd, I actually like it.

1 Like

I’m not a fan of this syntax (or putting the ? as a prefix operator):
if someVariable? {

}

It reminds me of the Swift 1.0 (or pre 1.0) going into a block if you have a non-nil value which was removed because of the confusion with Bool? it would enter the block if the value was true or false but not nil.

I feel like making the binding slightly more explicit with at least a var/let would help make it more clear from a maintainability standpoint and it wouldn’t be as confusing to a newcomer.

Something more like this or a variation would be nice:
if let someVariable? {

}

2 Likes

An interesting discussion — thanks @chockenberry for kicking it off, I saw your original tweet but have come here for another reason thanks to @rlziii and @DeFrenZ !

I favour the "ditto" style approach but with a different syntax, and with the "ditto" keyword/operator whatever that may be, being on the right side because this solves aforementioned issues about expressions that can be on the right side that wouldn't map well to a new local variable, but more importantly: this becomes useful in many more locations as a feature to avoid boilerplate, such as the very common horror of initialiser implementations that have to copy every variable into self.

init(fileSystemPath: String, identifier: String) {
   self.fileSystemPath = fileSystemPath
   self.identifier = identifier
}

…becomes something like:

init(fileSystemPath: String, identifier: String) {
   self.fileSystemPath = *
   self.identifier = *
}

...where * or whatever symbol is chosen in this case would evaluate to the variable in local scope the same as the name as the lvalue of the expression, or the argument name:

doSomething(fileSystemPath: *, identifier: *)

IMO this is incredibly handy and will also map well to if let, for whatever symbol is chosen:

if let foo = * {
}

It feels like if we had the above sugar for let we should have it for all assignments and argument passing, so any solution should work for both.

Some might complain about * but it is not a concrete suggestion. @ might work I suppose but is likely reserved for attributes. $ maybe.

Anyway, I am a hard "no way" on the let '' = x style as this is just way too easy to confuse with empty strings and suffers the problem of the RHS not providing a viable var name for the LHS.

11 Likes

In the search for simple symbols to use... I'm sure there are terrible problems with using it but... wouldn't a period . make quite a lot of sense, in the filesystem/XPath etc. sense of "where we are":

if let x = . {

}

doSomething(longParameter: .)

I presume this would be too nasty re: implicit member resolution of enums etc. but ... it's not a million miles from the same idea? I am not sure I love how it looks but :man_shrugging:

Not sure if syntax is okay but wow I'm loving this.

4 Likes

Does that mean we aren't allowed to consider it (or some subset) as a potential solution to this problem?