Problems with closure type inference

Interesting situation:

class A<U> {
    func then(execute body: () -> ()) {}
}
class B<U> {
//    func then(execute body: () -> ()) {}
    func then(execute body: (U) -> ()) {}
}

func first<U>(execute body: () -> A<U>) -> A<U> { return A<U>() }
func first<T>(execute body: () -> B<T>) -> B<T> { return B<T>() }

func foo() -> A<Int> { return A<Int>() }
first { 
    let y = 0
    return foo() // Cannot convert return expression of type 'A<Int>' to return type 'B<()>'
}.then {}

first {
    let y = 0
    return foo()
}.then { arg in } // Contextual closure type '() -> ()' expects 0 arguments, but 1 was used in closure body
// This means the compiler is well aware of what's going on ^

first { // OK
    foo()
}.then {}

first { // OK
    foo()
}//.then {}
class B<U> {
    func then(execute body: () -> ()) {}
//    func then(execute body: (U) -> ()) {}
}

first {
    let y = 0
    return foo()
}.then {} // Cannot invoke 'then' with an argument list of type '(() -> ())'

Without generics:

class A {
    func then(execute body: () -> ()) {}
}
class B {
//    func then(execute body: () -> ()) {}
    func then(execute body: (B) -> ()) {}
}

func first(execute body: () -> A) -> A { return A() }
func first(execute body: () -> B) -> B { return B() }

func foo() -> A { return A() }

first { // OK
    let y = 0
    return foo()
}.then {}

first { // Ambiguous use of 'first(execute:)'
    let y = 0
    return foo()
}//.then {}

Intuition tells me this is related to the typical cannot infer complex closure return type limitation. Some frameworks (perhaps you already realized which) are having a hard time with exploiting these trivial cases. Worth filing?

I was also wondering whether finding return in a closure is a task hard enough to

let bool = { // cannot infer complex closure return type
    let a = 0
    return true
}

If a closure has multiple statements in it, its body is not involved in determining its type. This restriction goes all the way back to Swift 1.

I see… That explains everything. Performance issues?

Part of it’s to avoid massive constraint systems, yes, but it’s also to keep diagnostics from going completely off the rails (they are pretty bad in your examples, but they could be even worse), and to have a simpler model where a statement is type-checked in full before moving on to the next statement.

Other languages do have whole-function type inference, and you might wonder how they can get away with it. The answer is usually that those languages don’t have overloading, which means that there aren’t multiple ways to interpret function calls.

5 Likes

There is still something that isn’t clear to me. Consider the following example. The compiler has to check whether the closure is of a valid type, so I assume the body is involved in type-checking. Is there a big difference between checking the type and determining it?

func foo(_ arg: () -> Bool) {}

foo { // OK
    let foo = 0
    return true
}

The difference is between “determining the type of the closure” and “checking the body of the closure”. For a multi-statement closure, the type is determined first, using just the information in the signature, if present (the part before the in) along with any contextual information—in this case, the call to foo. The compiler will then come back and use that to type-check the body later. For a single-expression closure, the two steps are performed together, allowing the body to influence the final closure type.

1 Like

Ah, so this situation only happens when the compiler, using the information in the signature and contextual information, can’t derive the type of the closure at all or without further inspecting it’s body.

func foo(_ arg: () -> Bool) {}
func foo(_ arg: () -> Int) {}

foo { // ambiguous without inspecting the body
    let foo = 0
    return true
}

func foo1(_ arg: (Bool) -> Bool) {}
func foo1(_ arg: (Int) -> Int) {}

foo1 { (arg: Bool) in // OK
    let foo = 0
    return true
}

func foo2(_ arg: (Bool, Int) -> Bool) {}
func foo2(_ arg: (Int, Int) -> Int) {}

foo2 { (arg1: Bool, arg2) in // OK
    let foo = 0
    return true
}

var bool = { _ in return true } // truly ambiguous

I still don’t understand why, given the argument type, inspecting the closure and then checking if it is a valid call based on the return type is such a big deal. But that seems to be a long conversation. If the main reason though is that there are much more complex cases that would have to be considered, everything is clear.

func foo2(_ arg: () -> Bool) {}
func foo2(_ arg: () -> Int) {}

foo { // The argument type is Void, the return type is Bool,
      //  let's see if we have a corresponding foo with one argument that accepts () -> Bool.
      //  If we don't, ambiguous call.
    let foo = 0
    return true
}
1 Like