Swift 5 throwing and non throwing function

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.

I've reduced the problem down to this:

func foo() throws -> Int {
    print("Throwing function")
    return 1
}

func foo() -> Int? {
    print("Non throwing function")
    return 0
}

let i: Int? = try? foo()

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.

9 Likes

thanks for your speedy reply!

ah ok, that makes sense. Just curious why the type-checker prefers the non throwing function?

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.

2 Likes

Does narrower mean more specific? I thought the type checker would prefer the most specific overload?

Yes, a non-throwing function is considered a specific case of a throwing function: it is more limited in its output

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.

Maybe it's preferring the non-throwing version for some other reason. I guess the throwing version has an additional promotion to optional?

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...

It would be great to have some naming guidance for the throwing and future async additions in a similar fashion as C# encourages the Async suffix.