One of the things that makes writing Swift so fluid is that the Swift type inference system is strong enough for us to write type-safe code from a natural perspective, without having to pepper String
and Int
everywhere. Even with somewhat complex typing, such as generics, the compiler will know what to do.
However, once in a while you'll hit upon a case where the compiler screeches to a halt, wrenching you back into its concrete world of step-by-step type checking.
Swift Type Inference Operates on Single Statements
A quick exercise: Can you tell me the type of the first closure (with elem
) in the following code? How about the second closure (with elem2
)?
let a = [[1,2],[3],[4,5,6]]
var b: [Int]
b = a.flatMap { elem in
return elem
}
b = a.flatMap { elem2 in
print(elem2)
return elem2
}
For a human, it's easy to see that both closures have the same type. Adding a print statement won't affect the input or return value, certainly! We can see that an implementation of flatMap
here is valid for the type ([Int]) -> [Int]
.
However, trying to compile this code will result in an error on the return elem2
line:
error: cannot convert return expression of type '[Int]' to return type 'Int?'
What's going on here? Why does the compiler admit the first closure, but balk at the second?
Well, it turns out that the Swift type inference system only works for single statements as a whole. Since the second closure has two statements, type-inferring is skipped altogether. Slava Pestov explains on the swift-dev mailing list:
While conceptually, it would not be a huge change to the type checker algorithm to support global type inference for closures and other functions consisting of multiple statements, we feel that philosophically, this is not a direction the language should take. Global type inference will further exacerbate performance problems with type checking, as well as make it harder to produce good diagnostics.
Looking at the documentation, we can see that there are two definitions of flatMap
with different types (one, two). Instead of trying to infer what types we mean in the second flatMap
closure, the type checker just picks an implementation of flatMap
(the first one linked) and checks against those types. If we explicitly declare the types in our closure, the type checker will match it with the implementation we want, and everything works again:
b = a.flatMap { (elem2: [Int]) -> [Int] in
print(elem2)
return elem2
}
Swift's Open Philosophy Makes it Better
While the decision to limit type inference makes sense, the error message is baffling unless you happen to know this esoteric detail about the compiler. It tells you you're doing something wrong, when you know your code is valid.
I suggested adding a helpful message explaining this, whenever the type checker encounters a type error in a closure that it didn't infer types for. Something along the lines of
Did not infer types in multi-statement closure, try explicitly declaring them if you are expecting a different implementation.
I was asked to file an issue with this recommendation, which I did here.
In the course of an afternoon, I helped identify some strange behavior, received an explanation, and offered an improvement that will make the experience better for future Swift users. I'm not a compiler expert, nor am I a part of the official Swift team. I'm just an interested developer, and I love the fact that Swift can crowdsource improvements, however minor, and keep getting a little better than we left it yesterday.