flatMap on UIView.constraints type inference confusion with no return type change in closure

Hi everyone,

Have what seems like a type inference bug in UIKit / NSLayoutConstraint. The original extension (with simplification) is:

extension UIView {

    func flatMapInvalid() -> [NSLayoutConstraint] {
        [self].flatMap { return $0.constraints }
    }
}

This compiles. However, making even the most minor no-op change to the flatMap() closure causes something strange with type inference:

    func revealsFlatMapInvalid() -> [NSLayoutConstraint] {
        [self].flatMap { let _ = 0; return $0.constraints }
    }

Note adding the let _ = 0. This could be any valid line of code, however, it doesn't matter. The error then is:

Cannot convert return expression of type '[NSLayoutConstraint]' to return type 'NSLayoutConstraint?'

So the question is, why would inserting a line of code before the return cause the compiler to expect Optional<NSLayoutConstraint>, whereas the original accepted [NSLayoutConstraint]?

Clearer, in my opinion:

    func reduceCorrectIntent() -> [NSLayoutConstraint] {
        [self].reduce([NSLayoutConstraint]()) { let _ = 0; return $0 + $1.constraints }
    }

So not sure if this is a bug, unavoidable, or something obvious I'm not seeing.

Thanks in advance

This behavior occurs because:

If a closure contains a single expression, Swift will consider its body in addition to its signature and the surrounding context when performing type inference. [...] If a closure body is not a single expression, it will not be considered when inferring the closure type.

The reason for this is as follows:

See the compiler's educational note for closure type inference for more information on how to address this situation in your code.

Hi there,

Thanks @xwu for the authoritative response! I didn't think it was likely I'd uncovered some super-hidden obscure bug :smiley:

Note also that the compiler normally can infer multi-line closure using context from the outside. To wit,

extension Array {
    func xx<Result>(block: (Element) -> [Result]) -> [Result] {
        flatMap(block)
    }
}

extension UIView {
    func flatMapInvalid() -> [NSLayoutConstraint] {
        [self].xx { let _ = 0; return $0.constraints } // ok
    }
}

From this, we already know that block closure is of type (Element) -> [Result] from the xx definition. We then know that Element == UIView from [self] portion and that [Result] == [NSLayoutConstraint] from the function return type. So the compiler can indeed infer that xx.block is of type (UIView) -> [NSLayoutConstraint] without ever looking into the closure body (which it won't).

The problem with flatMap is that there are two ambiguous definitions the deprecated one returning optional, and the new one, so the compiler is left with two different function types in the end before requiring to look into the closure body.

It could still be a bug, though, since normally the error would be more useful:

Unable to infer complex return type; add explicit type to disambiguate