We've just updated our app from Swift 4.2 to Swift 5, both on Xcode 10.3, (yep a bit delayed..) It uses the 3rd party library JSONUtilities framework, which contains code like this.
Specifically two functions called the same, a throwing function that returns a non optional and a non throwing function that wraps the throwing function returning an optional.
Previously in Swift 4.2 this just worked, if you put a try in front of the function it would resolve to the throwing function otherwise it would resolve to the non throwing function.
In Swift 4.2 this works and prints "throwing function". However in Swift 5, it prints "non throwing function" and there is a warning on the last line that "No calls to throwing functions occur within 'try' expression".
While the code seems a bit contrived and pointless, in JSONUtilities it leads to an infinite loop. as the code is basically:
func bar() throws -> Int {
print("Throwing function")
return 1
}
func bar() -> Int? {
return try? bar() // <- Infinite loop here
}
let i: Int? = bar()
On the line I've commented on Swift 4.2 it calls the throwing function, on Swift 5 it calls itself.
I realise that maybe the framework shouldn't offer such a shorthand, and that it can be fixed by appending as Int to the end of the line. But it seems worrying that the behaviour has changed like this and caused an infinite loop just by updated the Swift 5 version (with no code changes). For a try expression shouldn’t the compiler resolve to the throwing function if there is one?
try has never influenced overload resolution like that. What you are seeing is the impact of SE-0230, which caused try? to force its result to be optional but not necessarily add another level of optionality. Previously, the result of try? bar() using the non-throwing function would have had type Int??, which of course cannot be converted to Int?, and so overload resolution had no choice but to use the throwing function. With SE-0230, the result of try? bar() using the non-throwing function can still have type Int?, allowing it to be selected, which the type-checker prefers. Code using the Swift 4.2 language mode should still have the old behavior, but if you told Xcode to switch to using the Swift 5 language mode, you can see a behavior change in rare cases like this.
Overloading based only on throws-ness and return types is very fraught for exactly these kinds of reasons, and we strongly discourage doing so.
The general principle is that not throwing makes it a narrower type. It in fact doesn't have a narrower return type, but that's given secondary rank in the algorithm.
Does the throwing-ness of a function participate in ranking at all? I don't see anything in CSRanking.cpp from a quick glance, but I might be missing some check elsewhere of course.
There’s a check in matchFunctionTypes where we treat a non throwing function to be a subtype of a throwing one, but I’m not sure if that’s being used to compare overloads as well.
That would kick in when we compare arguments of function type -- there are some isConvertibleTo() checks in CSRanking that look at argument types. However, we're not doing this for the top-level function type of the overload AFAIK.
Hmm I think it's because of value-to-optional conversion since solver now finds two viable solutions - one of which is ranked lower due to it. This is for the first example (let i: Int? = try? foo()). In Swift 4.2, we only found one viable solution (which didn't have that conversion). I couldn't find anything in the solver that uses throws for ranking overloads like that...