Let's fix `if let` syntax

Hello all. I used to be a proponent of team if let x { … } till I read a well-considered proposal draft regarding unwrap that @Erica_Sadun once linked me to. I think the Unwrappable protocol introduced in that proposal can go beyond the specific use case being discussed in this here thread.

On a tangential note, it’s heartening to see Chris being open to reconsidering a decision made years ago, now with the benefit of almost six years of the Swift community’s collective experience.

8 Likes

I have some encountered feelings with this. I see all the positive words on the thread so is probably just me being on the wrong side but is not that clear to me that changing it is all positive. So I will just leave my 2c here.

I'm specially around this feeling of hiding even more the concept. of optionals. I think they are a very basic building block of the language and shouldn't be hidden more. In my experience, learning them is more of a struggle for people with baggage from other languages than newcomers, and really hiding them even more would just make them even harder to understand. if let = is already sugar, but is one that shows you what is going on: you are combining a check (if) and a binding (let =), which helps to understand that there is no magic here. I think that getting rid of that will just make optionals more confusing.

Furthermore, yes this is a very common pattern, but there is also tons of other code that won't benefit from this. And then you will end up with multiple ways of really doing the same thing, which is not bad per se but I feel that in this case it would be detrimental.

Using typescript's approach of making things non-nil contextually (after a x !=nil) is the change that looks cleaner to me since it doesn't add any special syntax, it just makes the compiler smarter and is still easy to understand. But it also feels kind of weird in Swift that types change without being explicit. And personally I'm not a fan of seeing nil in the code, it just confuses things even more for people that comes from languages with null.

Ultimately this is something I haven't found to be a problem after all the Swift code I've written since day 1. Is one of these things that I feel like if the tools were a bit better we wouldn't even be complaining.

5 Likes

My impression was that Unwrappable was much more specific to Optionals, e.g. most other monoidal types (Promises, Collections, State, IO, Either, and so-on) would either be completely unable to support Unwrappable or at least having ambiguities.

E.g. a result might have if unwrap result.success to clarify against if unwrap result.error, but since the point is to shadow the variable result neither of these work. You might instead need if try unwrap result to represent both flows.

Likewise with a task handle/promise, it would be something like if async unwrap result to account for the possibility that the task has not completed yet.

Independently, Optional has the benefit of automatic conversion from type T to optional T? when needed. This means even when shadowing a value with an if let statement, you can still use use that value in contexts that expect an optional version. Mapping the success of a result or the eventual value of a future would likely not have that compiler support for automatic conversion, making them less useful in this sort of context.

Isn't this more of

  • Shorthand syntactic sugar for 'variable shadowing'

rather than if let, guard let or case let etc?

Because I mean, type casting if let v = v as? OtherType { ... } is not included in this discussion, is it?
Also as mentioned by @ensan-hcl , if a value is chained, it would not work.


Keyword unwrap sounds natural to me as it is already used in stdlib:

Something like this?
If new syntax must be used with existing if keywords:

let optionalValue: Int?

if unwrapped optionalValue { ... } // <- Past tense sounds more natural, I guess
// or
if unwrap optionalValue { ... }

guard unwrapped optionalValue else { ... }
// or
guard unwrap optionalValue { ... }

However, it would be also nice if it can be used standalone:

let optionalValue: Int?

unwrap optionalValue { ... } // In this context present tense sounds natural
unwrap optionalValue else { ... } // awkward?

Similar thread:

3 Likes

That being the case, how about if case let x? {} as a shorthand for if case let x? = x {}? It won’t be immediately obvious to passing Typescript developers, but it’s reasonably terse and expresses the major points:

  • It’s a pattern match
  • It’s a binding pattern
  • It has something to do with optionals.
4 Likes

Here's a half-baked idea. We usually talk about moving complexity out of the compiler and making the standard library more capable. Since Optional wrapping is causing the confusion here (which happens to be a standard library type), instead of introducing new keywords we can define new methods on Optional that the compiler can use to make decisions and do variable assignments.

if x.unwrap(to: let y) { 
    print(y) // not optional
}
// y is inaccessible here, as it was defined inside the scope of the if statement

where unwrap is

extension Optional {
    func unwrap(to unwrapped: inout Wrapped) -> Bool
}

That won't compose well without compiler support.

if x.unwrap(to: let y), where y > 5 { // <-- how is y in scope here?
  ...
}
1 Like

I don't love the current if let syntax, but this feels like it is fixing a very specific special case, where I'd rather see a more general addition to the language which would let us fix this ourselves

for example; If a function was allowed to access it's caller, then we could write

func ifPresent(_ block: (Any)->Void) {
    if let nonNil = #caller {
        block(nonNil)
    }
}

which could be used as

foo?.ifPresent() {
  $0.rect = .zero
}

for bonus points, we could have a way of defining default variable names when calling functions. Throw in access to the caller's string name, and you have

func ifPresent(_ block: (Any)->Void) {
    if let nonNil = #caller {
        block(nonNil @VariableDefaultName(#caller_string_name))
    }
}

which could be used as

foo?.ifPresent() {
  foo.rect = .zero
}

or

foo?.ifPresent() {
  preferredName in
  preferredName.rect = .zero
}

there are a bunch of other places where @VariableDefaultName would be great.

for example

networkRequest.run {
   /// @VariableDefaultName has already implicitly written
   /// request, data, error in 
  if error.isNil {
   data.process()
 }
}

note: we wouldn't need access to #caller if there was a way to write

extension Any {
  func someFunc() {
   //I can now access self, rather than needing #caller
  }
}

but I don't think that is currently possible.

1 Like

The current if let syntax creates a new variable. It is not necessary to have access to the caller, because the bound variable in an if let is a copy anyway.

You can write something like

extension Optional {
    func ifPresent(_ block: (Wrapped) -> Void) {
        if let wrapped = self {
            block(wrapped)
        }
    }
}

and all you're missing is the ability to make the block parameter read-write (var).

3 Likes

From this, I'd also like to say that new syntax sugar should imply this limitation. If we chose have for example, then the next code would seem natural. But it would not work.

if have data.value { /* ... */ }

Likewise, if exist data.value, if unwrap data.value, if data.value?, if nonnil data.value all seem natural but would not work.

New syntax should contain let. The next syntax seems weird, because in Swift no expression after let is a property. It is great that impossible thing seems weird, isn't it?

// it is impossible to declare property or function
let data.value = 42 
let function() = 42

// it is also impossible!
if let data.value { /* ... */ }
if let function() { /* ... */ }

It is also good because it can declare mutability. To use let and var, possible and readable ways would be if let x {}, if let? x {}, if let x? {}. I think it's okay to insert keyword before let/var like if unwrap let x {}, though it makes the code longer. As @benrimmington mentioned, if let '' = x {} is also good from this aspect.

4 Likes

good point. Extending via optional gets you a chunk of the way there and you don't need #caller

I'd still love to have @VariableDefaultName() and #caller_string_name
I think they could really tidy up a bunch of interfaces

extension Optional {
    func ifPresent(_ block: (Wrapped) -> Void) {
        if let wrapped = self {
            block(wrapped @VariableDefaultName(#caller_string_name))
        }
    }
}

which allows

foo.ifPresent() {
  foo.thing = .zero
}

(at least where foo is a class...)

@VariableDefaultName() would also be great in so many other situations - e.g. having to explicitly provide variable names on any AFNetworking call just seems painful.

My extension already allows that:

class C {
    var x: Int = 0
}

let c: C? = C()

c.ifPresent { (c) in
    c.x = 19
}

print(c?.x) // Optional(19)

not quite - you still have to manually rename your variable in the block

foo.ifPresent() {
  foo.thing = .zero
}

vs

foo.ifPresent() {
  foo in // <- this
  foo.thing = .zero
}

having to rename manually means that after a refactoring, you'll have

bar.ifPresent() {
  foo in // <- this
  foo.thing = .zero
}

and it's also extra boilerplate

I see. I missed the nuance. It's an interesting idea, but I'm not sure how much it generalizes, to the point that it would be worthwhile sugar.

I think what you're looking for is a mechanism to signal that the object on which a method is invoked should be available as the sole parameter of the method.

The problem is that this breaks down:

Optional(3).ifPresent {
   // What is the parameter name?
}
1 Like

yes - in this case #caller_string_name would be nil, and you'd have to fall back to the old method of explicitly providing a variable name or using $0

(you'd still write your extension the same way - you just wouldn't have access to an auto-named variable when you used it)

I'm not only interested in this situation though - I think @VariableDefaultName() could tidy up the API in a gazillion other areas (e.g. the networking library example above)

func returnNetworkingResult() {
   self.savedCallback(result,data,error @VariableDefaultNames("result","data","error")
}

which would be used like

libRequest.run() {
 // result,data,error in   ///Note I don't need this line
  if error == nil {
   self.handleResult(data)
  }
}

Swift has enough magic variables, but at least the names are defined by the language (e.g. newValue, error in catch blocks, etc). I can't see this idea getting off the ground. The caller might not even have visibility into the callee's source to find out which magic names exist.

2 Likes

Other ditto mark alternatives are two backticks or an underscore (as the initializer expression, on the right-hand side).

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

if let x = _ { /*...*/ }
2 Likes

I don't understand why everyone is trying to invent a different spelling here when if let x is seemingly unambiguous and so obvious to Swift developers that it has been repeatedly proposed/discussed in that form over the 6+ years I've been reading the Swift forums/mailing lists. If it's not going to be if let x then I'm going to struggle to support it.

That it doesn't naturally work to unwrap other.thing is a feature to me, because you should probably think about giving other.thing a different name in this scope (e.g. otherThing, specificThing), as opposed to if let x where x should already be a name that makes contextual sense.

22 Likes

that's a fair objection - though the compiler certainly would know those names and could show them in various ways (not least alt+click on the function which currently shows the definition)

Given that the longest form is

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

I would like to suggest

if let= value { }

Of course this looks unfamiliar but at least it contains the 3 important components 'let', '=' and 'value'...

2 Likes