`if let` shorthand

Good point – probably not. One of the reasons borrowing is more efficient is it doesn't need to bump a reference count, but this reference count bump is what strengthens the weak reference. So this is another case where if ref isn't always the right default, and instead users need the menu of all four options which can all be sugared consistently with this proposal.

3 Likes

One thing to note is that borrowing a property will still go through its usual stored, get/set, or read/modify interface to access the value of the property. Weak properties are effectively computed properties whose getter and setter capture a strong reference to the object at the point of access, so if ref of a weak property would be effectively no different from if let, "borrowing" the gotten temporary strong reference instead of assigning it. Unlike if inout, which would serve a currently-impossible use case, I suspect if ref would have much more limited applicability working with copiable values in Swift, since an if let value's lifetime is generally obviously locally eclipsed by the lifetime of the optional it was bound out of, and we can borrow out of the optional storage already.

5 Likes

(All my opinion and not hard facts, since that seems a necessary disclaimer:)

I disagree. Introducing more introducers in place of let/var should work reasonably well, and the proposed shorthand does offer a natural place to slot them in, but mitigating the potential confusion (esp if the new introducers are not used that often) relies entirely on the surrounding syntax:

If I see ref x = y, even though I don't know what ref means, I can still recognize the "shape" of a declaration/assignment. I see a keyword, a new identifier, the crucial bit: an equals sign, and then whatever goes in. Even without any knowledge of ref, I can reasonably deduce that something similar to let or var must be going on here.

If, on the other hand, I see just if ref x, there is no shape to guide me to recognizing it as a declaration/assignment. I, knowing nothing about ref, might just as well take it to mean "if x is a reference in some way I don't know about". Same applies to inout.

The same applies even to the proposed shorthand for just let and var, although it is perhaps less severe thanks to the fact that due to their pervasiveness in other Swift code, even novice users will quickly associate let and var with declarations.

And this is just the part about it being a declaration/assignment. The fact that optionality is somehow involved is completely obscured, relying only on first having learned about if let, and then recognizing the very distant similarity of if let foo = bar to if ref x, and this part is a problem with the shorthand even ignoring new introducers, and, tbh, even with the current if let syntax.

4 Likes

Right, but you had to learn the shape of assignments in Swift, and of optional unwrapping. This mainly seems like an argument from experience, not something generalizable to the language. There is no natural syntax for these operations, all of the forms have to be learned. If a user is taught "Use if let x = x to unwrap an optional", the natural follow up is "If you find that tedious, you can use if let x to unwrap into a value of the same name." This would apply to new keywords as well (though I kind of prefer if they were just versions of let and var: let(ref), var(inout)).

1 Like

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