Can we unlock non-optional constants / variables in comma separated conditional statements?

I recently wrote some code that required an intermediate non-optional constant inside a comma separated if statement to reduce boilerplate code, but the compiler didn't let me do it for no real reason.

// before
if 
  let someValue = someOptional,
  let foo = someFooFunction(someValue.with.some.really.long.chain),
  let bar = someBarFunction(someValue.with.some.really.long.chain)
{
  // use `someValue `, `foo` and `bar`
}

// after
if 
  let someValue = someOptional,
  let intermediate = someValue.with.some.really.long.chain, // non-optional
  let foo = someFooFunction(intermediate),
  let bar = someBarFunction(intermediate)
{
  // use `someValue `, `foo` and `bar`
}

I personally think that this first example has a similar motivation behind it like toggle() had.

Here's another theoretical example:

let optionalArray: [Int]? = ...

// status quo 1
if 
  let array = optionalArray, 
  let foo = someFooFunction(array.reversed()), 
  let bar = someBarFunction(array.reversed()) 
{ 
  // use `array`, `foo` and `bar`
} else {
  ...
}

// status quo 2
if let array = optionalArray { 
  let reversedArray array.reversed()
  if let foo = someFooFunction(reversedArray), let bar = someBarFunction(reversedArray) {
    // use `array`, `foo` and `bar`
  } else {
    // potential duplication of the other `else` branch
  }
} else {
  ...
}

// status quo 3 (example provided by @danielctull)
if 
  let array = optionalArray, 
  let reversedArray = .some(array.reversed()),  // wrap then unwrap the value as a workaround
  let foo = someFooFunction(reversedArray),
  let bar = someBarFunction(reversedArray) 
{ 
  // use `array`, `foo` and `bar`
} else {
  ...
}

// after
if 
  let array = optionalArray, 
  let reversedArray = array.reversed(),  // non optional
  let foo = someFooFunction(reversedArray),
  let bar = someBarFunction(reversedArray) 
{ 
  // use `array`, `foo` and `bar`
} else {
  ...
}

What do you think?

9 Likes

I would love this. However, your second motivating example isn't as good.

You could simply have

if let reversed = array?.reversed() {
  ...
}

It would be better if the example used both array and reversedArray in later components of the expression.

1 Like

@danielctull already provided similar feedback to me on Slack regarding the let reversed and that's why I explicitly added // use array, foo and bar to the branches to showcase that the unwrapped array is still needed.

1 Like

What is different here than adding the case keyword?

3 Likes

Uff, I didn't even know that you can write this.

// status quo 4
if
  let array = optionalArray,
  case let reversedArray: [Int] = array.reversed(), // workaround
  let foo = someFooFunction(reversedArray),
  let bar = someBarFunction(reversedArray)
{
  // use `array`, `foo` and `bar`
} else {
  ...
}

This solution is similarly counterintuitive just like the one where you wrap the value into an optional an unwrap it. We still need to jump through hoops instead of naturally create a simple constant.

Nah, the if let shorthand itself is the problem. Otherwise this monster bike shed wouldn't exist. They never should have allowed taking the case and ? out of the expression. People would intuitively know how to deal with what your thread is about if we always had to use the full form.

if
  case let array? = optionalArray,
  case let reversedArray: [Int] = array.reversed(),
  case let foo? = someFooFunction(reversedArray),
  case let bar? = someBarFunction(reversedArray)
{
10 Likes

Wow this just show cases how complex this situation has become. To be fair with you, I find the case syntax very unintuitive and unnatural for that kind of task. To me it fits more into pattern matching enums, nothing more nothing less, but anyone is free to disagree with me on that.

7 Likes

That workaround is already present in the second example as being the "status quo 3".

Woops, didn't see the scrollbar.

1 Like

Optional unwrapping is pattern-matching enums, though.

6 Likes

Honestly after years working with Swift I never considered to view optional unwrapping in conditional statements from that point of view. That basically means that if let a = optionalA is a sugar for if case .some(let a) = optionalA. :face_with_hand_over_mouth:

Regardless of all that, I still think that non-optional constants and variables should be be allowed to be placed into the comma separated conditional statement's list. Those won't hurt anyone and they would provide / unlock more flexibility in our code.

if 
  case .some(let a) = optionalA,
  let b = a.with.some.really.long.path, // pure assignment of the constant `b` - no pattern matching
  case .some(let foo) = someFooFunction(b),
  case .some(let bar) = someBarFunction(b)
{
  // use `a`, `foo` and `bar` here
}

Oh dang, I already see a potential issue with this, but I don't know if it's a "real" roadblock here.

It's not possible to just extract the value from the chain as if case it's another optional, it will be treated just as another optional unwrapping. :thinking:

I also find the lack of support for non-optionals counter-intuitive. I think the reason is that it would enable the following anti-pattern:

let a = 42

if let answer = a {
  // use answer
}

The example above is totally meaningless and without additional checks by the compiler to constrain when non-optionals could be used with if let it would be allowed.

2 Likes

Wouldn't it be sufficient to let the compiler check if there is at least one pattern or a boolean expression included?

1 Like

Yes, it would. I’m not sure if the end result is less counter-intuitive however.

I don’t see how that is any worse than either of these, which are perfectly valid Swift today:

let a = 42

if let answer = Optional(a) {
  // use answer
}

if let answer = .some(a) {
  // use answer
}

Indeed, Swift already has optional-promotion, where non-optional values are implicitly promoted to Optional when the context requires it.

Therefore it is actively surprising that if let answer = a does not already work. One would expect the compiler to see the context of if let requires an optional, and thus implicitly promote a to Optional just like it would in other places.

2 Likes

In your examples, you’re actively working around the language and it complies, as it should. I also don’t think that implicitly promoting a non-optional to an optional to motivate the new syntax makes sense — Swift has been resisting implicit conversions forever.

1 Like

I hate if let, but I hate the idea being discussed here more. :smile_cat: if let and guard let have specific meaning. Wondering whether the expression coming after yields a conditional result would be a waste of time, option-clicking on the end of a chain to find out if an unwrapping is happening. I've used case let for years. It was weird, and then it wasn't. Let yourself pass through the painful unfamiliar stage and embrace it.

It is easier to love if you first give for case let a hug. :hugs:

4 Likes

The question is though, would you support the general idea iff we did not had if let up until now and already worked with if case let instead?

Meaning, that b extracted from path would remain an optional if it was an optional.

if 
  case .some(let a) = optionalA,
  let b = a.with.some.really.long.path,
  case .some(let foo) = someFooFunction(b),
  case .some(let bar) = someBarFunction(b)
{
  // use `a`, `foo` and `bar` here
}

I'm just generally curious, that's why I'm asking for peoples opinion and feedback.

1 Like

Everything you wrote here is false.

In my examples I am actively using existing features of the language.

Furthermore, promoting a non-optional to an optional is existing behavior. Swift has not “resisted” this implicit promotion to Optional, it has embraced it from the very beginning.

And, to be abundantly clear, if let is not “new syntax”: it is the existing syntax for unwrapping an optional, which is exactly how I used it.