Making the typechecker aware of if-let/guard-let optional initialization to reduce ambiguity

Hi! This arises out of discussion in SR-8347. Currently, if the initializer of an if-let binding doesn't result in an optional, you get an error:

func f() -> Int { return 0 }
if let n = f() {} // error: initializer for conditional binding must have Optional type, not 'Int'

However, the typechecking of the initializing expression itself doesn't have that information, which it could use to disambiguate, thus, also currently, this error:

func f() -> Int { return 0 }
func f() -> String? { return "" }
if let n = f() {} // error: ambiguous use of 'f()'

I'd like to update the type checker so that in the second code sample, it is aware that the result type of f() must by optional, and so the compiler will choose the overload of f() that returns a String?.

This seems pretty straightforward but as type inference is a tricksy little hobbit, it's a good idea to ask whether anyone sees a way in which this might go wrong with their existing code, or perhaps think of other constructs where a similar sort of behavior ought to occur?



This seems like a very reasonable thing to disambiguate for.

I wonder if people will also expect this to work or if there is a reasonable path to making it work?

func foo() -> Double { return 2 }
func foo() -> Int? { return 1 }

switch foo() {
case let x?:
  print("x = \(x)")
case nil:

I think this makes sense. I wonder if there's other places in the language that could disambiguate on the return type.

Also TIL that if let has an optional promotion feature:

func foo() -> Int { return 0 }

if let n: Int = foo() {

// Or more explicitly:
if let n: Int = 1 + 1 {

Granted it does provide a warning, but it's odd that adding a type annotation turns what is normally an error into a warning. I can't think of many places where Swift gives you the option to turn an error into a warning.


I'm against this change, because it would engrave the "initializer for conditional binding must have Optional type" error in the language forever.

And that would be a mistake, because this error has to go away eventually, as Swift gets mature.

We have all written if let v = nonOptional(), v.isFoo { ... }, and got a compiler error, because we did not really care about optional unwrapping but more about chaining initializations and tests.

Long ago, when Swift has started using the comma for splitting conditions behind if, a new language idiom was invented: the chain of initializations and tests. This idiom is fundamental in Swift fluidity. You can chain multiple computations, initializations, tests, and even use var instead of let when you plan to later modify a variable.

You know this chain. This is common and idiomatic Swift:

if let foo = foo,
    let bar = baz(),
    foo.qux > bar.qux,
{ ... }

But this idom is hindered by the "initializer for conditional binding must have Optional type" error.

For example, in the sample code above, the clear and concise condition has to be fully rewritten if the baz method stops returning an optional.

Enough said. My message is: this error is a youth error and must go away. We must thus not build on top of it.


You should be able to handle that by replacing let with case let - which apparently isn't very intuitive, but works with current and most likely future versions of the language.

Swift is quite restrictive with implicit conversions, and probably, that's a good thing: Optionals are the only big exception, and there always have been minor issues with that feature. So the current behavior can be seen as an exception from an exception.
It's probably too late for any fundamental changes, but even if the proposed feature is implemented, it would still be possible to allow both assignments, and prefer the one with the Optional.

1 Like

You are right about case let, @Tino, thanks.

But I've always found that this idiom is uncommon and puzzles readers: I'm not found of it at all, and never use it. It's a disservice to me, future me, and other readers.

If I were to pitch more the acceptance of if let for non optional values, I'd ask for a warning:

// warning
if let v = nonOptional() { ... }

// no warning
if let v = nonOptional(), v.isFoo { ... }

This is just food for thought.

Edit I basically ask for PEP-572 for Swift ;-)

1 Like

And I don't want to sound as if I wanted to block the exploration started by @gregtitus. Go ahead, folks, we never know what we'll find!

I'd be strongly against this. I'd rather have the ambiguity explicitly pointed out to me because the compiler can't be sure I meant it to use the String? version and not that I forgot the Int version is not optional. In the latter case, any error message will no longer be at the site where the error was made

For example with this change in place,

func f() -> Int { return 0 }
func f() -> String? { return "" }
if let n = f() // no error, the String? version is assumed
    // lots of code
    let y = n + 1 // error

As I pointed out earlier in the thread, you can technically already do this. You just have to add an explicit type annotation. I will say though, this feels like a bit of a hole in the type system.

if let n: Int = 1 + 1  { // compiles with a warning

This fundamentally seems to be an argument against overloading on return type generally, which should be a discussion for a different thread. I bet some Swift compiler developers, particularly anyone working on type checker performance, wish that this overloading wasn't allowed, but it is probably too late now. From a consistency standpoint I don't see why this change (which is really more of a bug fix) shouldn't be made.

How so? I don't think so.

Because you can be similarly confused about which version of a function is being called in any situation where return-type inference is used, and this can cause non-local errors. For example, you could be using your f() function as the T? parameter to any generic method, which is approximately what the if let construct is doing. Or, another example:

func f() -> Int { return 0 }
func f() -> String? { return "zero" }

func g() -> Int { return 1 }
func g() -> String { return "one" }

let x = f() ?? g()

which compiles but might similarly confuse people about the type of x. These examples are directly analogous to the situation being discussed here, which is why I said that this should be allowed for the sake of consistency and that perhaps your argument should be against the general feature instead.

Wow, I didn't know that! This is super useful, thanks. (I agree it's highly unintuitive though)

Well no you can't be so confused because the compiler will flag the use as ambiguous.

func foo() -> Int { return 1 }
func foo() -> String? { return "1" }

let x = foo() // ambiguous use of foo error

I would argue that your example is a bug in the compiler. let x = f() ?? g() should be flagged as ambiguous. Anyway, the fact that this could lead to non local errors is not an argument for introducing more cases where non local errors can happen.

In some simple cases, but not when there is some context that leads the compiler to prefer one over the other. There is a deliberate set of rules in the compiler to prefer certain overloads to others. Relevant to this case, it generally prefers overloads where T doesn't need to be implicitly converted to T? in order to type check. You can easily implement your own approximate version of if let, e.g.

func f() -> Int { return 0 }
func f() -> String? { return "zero" }

func ifLet<T>(_ o: T?, body: (T) -> Void) {

ifLet(f()) { n in print(n) } // zero

which has the behaviour you would expect, given the current type checker implementation.

Sure, but ideally rules should be applied consistently so they are understandable to users. Why shouldn't the type checker prefer the optional overload in this case, where it doesn't need to do an implicit conversion, like it does in all these other situations? If you think this type checking behaviour is too confusing to be valuable then I still think that is an argument for another thread.

Well the rules are not being applied consistently now, otherwise this thread would not exist. It's nice to have consistency, but this is heading in a direction of consistently bad in my opinion.

I wouldn't expect this to work (disambiguating switch type based on cases), because Swift only tries to infer types with information from within the same statement, and I think the documentation and compiler are fairly consistent with that message, so I expect most other Swift coders have internalized it.

In theory, we maybe could make it work, since the compiler does a lot of work figuring out what type(s) are
covered by the case patterns while it is checking exhaustiveness of cases, and it looks like that is done independently of the switch expression type. So if the switch expression was ambiguous, you could do exhaustiveness checking, see what type is fully covered (or most closely covered) by the cases, and prefer that type for the switch.

It wouldn't be my first choice for extending type checking over multiple statements, though. I find multi-line closures is the only spot where the scope of type inference ever bites me in practice.

1 Like

It was my impression that you were advocating removing overloading on return type also, so clearly I'm misunderstanding. If you want jawbroken's example let x = f() ?? g() to also result in an ambiguity error (where the ?? operator must take an Optional as its left-hand-side argument), then under what circumstances, in your view, should it be valid to call a function like f() that has two overloads that differ only in return type?