`if let x as? T`

Now that we have if let x shortcut, shouldn't we also have:

if let value as? T

as a shortcut for

if let value = value as? T

As a small data point – in one of my big projects of all if-let-foos (hundreds of them) about 10% are of the form if-let-foo-as-T.

PS. I also wouldn't mind "if let foo.bar.baz" as a synonym for "if let baz = foo.bar.baz", but let's keep that separate.

13 Likes

Seems reasonable to me -- this was mentioned as a future direction in SE-0345:

A natural extension of this new syntax could be to support shorthand for optional casting. For example:

if let foo as? Bar { ... }

could be equivalent to:

if let foo = foo as? Bar { ... }

This is not included in this proposal, but is a reasonable feature that could be added in the future.

This wasn't included in SE-0345 because it makes the parsing and formal grammar a bit more complicated. I suppose at the time SE-0345 was still somewhat controversial, so there were bigger things to worry about.

In retrospect it seems perhaps a bit confusing to not support this, since it's visually very similar to the supported if let foo = foo case, and the = foo here isn't required for any semantic reason. Are there any other cases like this, that seem superficially related to if let foo = foo but which don't support the shorthand from SE-0345?

7 Likes

Should we still have the question mark after as in this shorthand? There isn't really anything related to optionals happening in this statement. The equivalent case condition case let value as T = value doesn't use the ? symbol at all.

if let value as T { ... }
3 Likes

x on its own isn’t an optional, so I think there needs to be something alluding to wrapping something in an optional for the if let to then unwrap.

2 Likes

Agreed.

Also, for if let foo = foo and if let foo I think of it as just eliding / omitting the redundant = foo, rather than using a different concept or syntax entirely.

If we apply the same to if let foo = foo as? T, it gives us if let foo as? T. Keeping this model would seem preferable for consistency and predictability.

4 Likes

I understand the appeal, but I don't think reusing the name is a good idea. By casting, we make something more specific, which deserves a more specific name:

if let cat = animal as? Cat {
    // ....
} else if let dog = animal as? Dog {
    // ....
} else {
    // ....
}
9 Likes

There are two forms being conflated in this idea.

When you're not casting, but actually dealing with an Optional, the progression of sugar is clear:

let value: Optional = 0
if case .some(let value) = value { }
if case let value? = value { }
if let value = value { }
if let value { }

But otherwise, there are two forms to shorten, depending on if you're adding optionality or not:

let value: Any = 0

if case .some(let value) = value as? Int { }
if case let value? = value as? Int { }
if let value = value as? Int { }

if case let value as Int = value { }

The latter form is not what was addressed in Swift 5.7. So while only what's already been proposed is necessary for consistency—and readability— I propose that shortened case forms should also compile:

let value: Any = 0
if let value as? Int { }
if case let value as Int { }
let value: Optional = 0
if case let value? { }

This is all with the caveat that the existing spelling has always been broken.

We should have gotten the proposed if let value? (leading to if let value? as? Int for this thread) instead, but we didn't, because what should have been in the language from the start,

if let value = value?

is instead

if let value = value

:crying_cat_face:

2 Likes

This seems quite similar to something I was just thinking about, except with comparators instead of casting.

For example, currently we might want to do the following when storing a previous value for comparison...

var previousMessage: String? = "foo"
var message = "bar"

if let previousMessage, previousMessage != message {
    print("message changed")
}

But this is quite noisy and can scale badly with multiple similar comparisons which need to unwrap optionals before they can be used.

Instead, you could imagine seeing this written as below and instinctively know what's going on IMHO...

var previousMessage: String? = "foo"
var message = "bar"

if let previousMessage != message {
    print("message changed")
}

This isn't currently supported and throws the error:

:x: Pattern variable binding cannot appear in an expression

Does anyone know if this kind of sugar has been discussed before?

I can't see anything after skimming over the proposal linked earlier for if let originally in SE-0345 about this.

Huh, an interesting twist...

I assume you do need the unwrapped previousMessage inside the block, as normal comparison works:

var previousMessage: String? = "foo"
var message = "bar"

if previousMessage != message {
    print("message changed")
}

Other operators as well?

if let previousMessage < message { ... }

Hey @tera :wave:t2:

I should have put in my original message but there's a functional difference with your observation of

if previousMessage != message

and my comparison

if let previousMessage, previousMessage != message

where yours won't check for non-nil values whereas mine is ensuring the value is non-nil as well as a different some-value.

My original code should have been expanded to include the nil check, eg:

var previousMessage: String? = "foo"
var message = "bar"

if previousMessage != nil, previousMessage != message {
    print("message changed")
}

Perhaps a bad/overly simple example on my part in my original code.

Another example is you could imagine this being even more useful in a guard in real code where it's more typical to do if let combined with some comparison before bailing out early, especially in more complex/real world code which might have many guards with more complex comparisons.

var previousMessage: String? = "foo"
var message = "bar"

guard let previousMessage != message else {
    return
}

print("message changed")

This can be quite confusing if second value also optional:

var previousMessage: String? = "foo"
var message: String? = "bar"

if let previousMessage != let message { // what is happening here?
    print("message changed")
}
var previousMessage: String? = "foo"
var message: String? = "bar"

if let previousMessage != let message { // what is happening here?
    print("message changed")
}

I wouldn't say this code was confusing, maybe just unusual at first?

The only possible confusion I would see is if you drop the second let and then it's not quite clear if the value on the RHS is unwrapped or if it's still the original value, eg:

var previousMessage: String? = "foo"
var message: String? = "bar"

if let previousMessage != message { // is `message` String? or String
    print("message changed")
}

But my intuition looking at that is without the let it's simply not unwrapped and is actually still a String?

I mean different executing paths will be in the following cases:

var previousMessage: String? = "foo"
var message: String? = "bar"

if let previousMessage != message {
  // previousMessage is compared to message no matter message contains a value or nil
} else {
  // executed when:
  // - previousMessage != message's wrapped value
  // - previousMessage != message because message is nil
}

if let previousMessage != let message {
  // previousMessage is compared to message if message is only a `case .some(wrapped)`
} else {
  // executed when:
  // - previousMessage != message's wrapped value
  // - previousMessage != message because message is nil
}

While the else body in both examples is executed by the same conditions, the equality operation in if-expression is executed differently, which may be unexpected and an can accidentally happen due to inattention. These two pieces of code look very similar but do different things.

I would prefer to write this code explicitly making my intent clear:

if let previousMessage, previousMessage != message {
} else {
}

if let message, previousMessage != message {
} else {
}

if let previousMessage, let message, previousMessage != message {
} else {
}

if previousMessage != message {
} else {
}

Separation of if-let unwrapping and further usage of unwrapped values seems more understandable and less error prone to me.

Indeed. I warned that if let foo { ... } read as nonsense, now I see we're in danger of sliding further down that slippery slope :wink:

:parachute: