`if let` shorthand

I would also suggest not getting hung up on the specific keyword ref, which is just a placeholder suggestion for the concept of a shared borrow binding. The exact keyword would end up getting extensive discussion, into which could feed the desire to make it sit well with this sugar.

7 Likes

May I ask for an elaboration, directions such as this: A roadmap for improving Swift performance predictability: ARC improvements and ownership control might (potentially) change or affect how if let or guard let etc. behave – So it is worth waiting to see that settled first... then this topic next. Am I in the right direction?

At least this thread proves once and for all that the amount of energy people spend debating an issue is inversely proportional to its importance.

5 Likes

How about if some foo { ... }. It's consistent with the .some case in Optional. The fact that let unwraps is magical and can be unintuitive. Teaching young programmers who aren't familiar with Optional I've always had to tell them "this is what let does here, it unwraps, you just have to know it. Yes, that's inconsistent with what let does outside of if and guard statements.". But if one teaches Optional from its cases none and some(Wrapped) then if some foo { ... } makes a lot of sense.

Only issue is the word some is being used to refer to protocols these days as well, like some View, but I think this is a different context.

3 Likes

Wouldn't that beg the opposite case, like if none foo (i.e. if foo == nil) as well?

I wouldn't argue against it if people liked that. But in my opinion, comparing foo to nil or .none with == is intuitive and simple enough, unlike using let to unwrap (not intuitive) or foo = foo (not as simple as it could be).

My suggestion of if some foo is just an alternative to the previous suggestion of if unwrap foo.

I like the if some foo syntax. I worry that reading Bool types will be confusing for someone learning the language.

var isImportant: Bool?

a thousand lines later…

if isImportant { majorDamage() }

Did you mean to post this in the review thread?

It would be nice to add a case for case ... where let:

func foo(action: String, userId: String?) {
    switch action {
        case "bar" where let userId:
            bar(userId)
        // ...
    }
}

func bar(_ userId: String) {
    // ...
}

At the current moment, this case can be solved in two ways:

  1. One of them that reviewers don't like:
case "bar" where userId != nil:
    bar(userId!)
  1. Another option is to simply use if:
case "bar":
    if let userId = userId {
        bar(userId)
    }
1 Like

The seems useful. It's worth noting that switch cases don't currently support optional binding conditions. For example, this doesn't compile today:

func foo(action: String, userId: String?) {
    switch action {
        case "bar" where let userId = userId: // 🛑 error: expected expression for 'where' guard of 'case'
            bar(userId)
        default:
          break
    }
}

func bar(_ userId: String) {
    // ...
}

I'm not really sure why switch cases don't support optional binding conditions. If we added support for them in the future, we would definitely want to support the shorthand syntax as well.

3 Likes

I think this is what you want.

func foo(action: String, userId: String?) {
    switch (action, userId) {
        case ("bar", let userId?):
            bar(userId)
        // ...
        default: break
    }
}

func bar(_ userId: String) {
    // ...
}
6 Likes

Thank you! This is a really good approach and I will use it, but in my case the code gets very noisy. I have 12 cases and only two of them use 2 different optional variables that need to be unwrapped:

switch (action, foo, bar) {
case ("a", _, _): ...
case ("b", _, _): ...
case ("c", _, _): ...
case ("d", _, _): ...
...
case ("x", let foo?, _): ...
case ("y", _, let bar?): ...
}

So, it would be very handy if it were possible to do where let foo:

switch action {
case "a": ...
case "b": ...
case "c": ...
case "d": ...
...
case "x" where let foo: ...
case "y" where let bar: ...
}
1 Like

I realize it's not the exact question that you asked, but, if the action value does not absolutely have to be a string, an enum with associated values might make a good choice:

enum Action {
    case a, b, c, d
    case x(String)
    case y(Int)
}

I am guessing that foo and bar are optional in your original code because most actions don't require an additional value, so callers of those actions provide nil, but the other two actions require a value.

If that is the case, an enum with associated values ensures that the actions that require a value have the necessary value, and the other actions don't have to worry about passing nils.

The switch statement ends up very close to what you were originally wanting and since action is an enum and not a string, the compiler can check to make sure every enum case is handled:

switch action {
case .a: ...
case .b: ...
case .c: ...
case .d: ...
...
case .x(let foo): ...
case .y(let bar): ...
}

If foo and bar really could be nil, then the associated value types could be optionals.

Again, I realize what I suggest isn't exactly what you were originally looking to do, but if the type of action is in your control, modeling the actions as an enum with associated values may be a good choice for a variety of reasons.

1 Like

Great job! I was thinking that the guard statement could be also improved by an extreme short syntax:

 // inferred `return` or `return nil` in this shortened version
guard condition == true

guard let x

instead of these very common and well-known paths:

guard condition == true else { return }
guard condition == true else { return nil }

guard let x = x else { return }
guard let x = x else { return nil }

IMO the short version is very readable and easy-to-write:

func getData(with query: String) -> Data? {
   
   guard query.isEmpty == false
   guard isValidQuery(query)

   return db.perform(query)
}
3 Likes

This is a commonly rejected change.

  • Infer return for omitted guard body: It has been proposed many times to allow omission of the guard body for the sake of brevity. However, a core principle of Swift is to make control flow explicit and visible. For example, the try keyword exists solely to indicate to the human reader where thrown errors can happen. Implicit returns would violate this principle, favoring terseness over clarity in a way that isn't typical of Swift. Furthermore, there are many ways of exiting the scope other than return (loops may want break or continue ), and not every function has an obvious default value to return.

True, but until about a week ago so was a shorthand syntax for if let foo = foo. ;)

That said, I think the reasons presented as to why inferring return is commonly rejected are good ones.

8 Likes

I see the point and thanks for the link. I think however that maybe in the future it could be reconsidered. It could be applied for example only in very clear scopes, in other ones it could be the compiler to warn the user etc.

2 Likes

In my case, it was literally necessary to process strings with optional parameters. More specifically, I refactored the code a bit regarding parsing deep links. This can be solved in many ways, but in this case I did not complicate the current implementation. I needed to define enum from a string which is a component of a path and query parameters:

enum DeeplinkEvent: Equatable {
    case presentPromoCode
    // ...
    case resetPassword(verificationCode: String)

    init?(_ rawValue: String, code: String?) {
        switch rawValue:
        case "promocode": 
            self = .presentPromoCode
        // ...
        case "reset-password" where code != nil: 
            self = .resetPassword(verificationCode: code!)
        default: return nil
    }
}
1 Like

Do we now support:

if let x as? Foo {   // if let x = x as? Foo

Or is that not included?

That syntax was not included in SE-0345, but was mentioned as a potential future direction.