Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift type inference in methods that can throw and cannot

As you may know, Swift can infer types from usage. For example, you can have overloaded methods that differ only in return type and freely use them as long as compiler is able to infer type. For example, with help of additional explicitly typed variable that will hold return value of such method.

I've found some funny moments. Imagine this class:

class MyClass {
    enum MyError: Error {
        case notImplemented
        case someException
    }

    func fun1() throws -> Any {
        throw MyError.notImplemented
    }

    func fun1() -> Int {
        return 1
    }

    func fun2() throws -> Any {
        throw MyError.notImplemented
    }

    func fun2() throws -> Int {
        if false {
            throw MyError.someException
        } else {
            return 2
        }
    }
}

Of course, it will work like:

let myClass = MyClass()
// let resul1 = myClass.fun1() // error: ambiguous use of 'fun1()'
let result1: Int = myClass.fun1() // OK

But next you can write something like:

// print(myClass.fun1()) // error: call can throw but is not marked with 'try'
// BUT
print(try? myClass.fun1()) // warning: no calls to throwing functions occur within 'try' expression

so it looks like mutual exclusive statements. Compiler tries to choose right function; with first call it tries to coerce cast from Int to Any, but what it's trying to do with second one?

Moreover, code like

if let result2 = try? myClass.fun2() { // No warnings
    print(result2)
}

will have no warning, so one may assume that compiler is able to choose right overload here (maybe based on fact, that one of the overloads actually returns nothing and only throws).

Am I right with my last assumption? Are warnings for fun1() logical? Do we have some tricks to fool compiler or to help it with type inference?

like image 522
Bohdan Ivanov Avatar asked Jan 29 '23 19:01

Bohdan Ivanov


1 Answers

Obviously you should never, ever write code like this. It's has way too many ways it can bite you, and as you see, it is. But let's see why.

First, try is just a decoration in Swift. It's not for the compiler. It's for you. The compiler works out all the types, and then determines whether a try was necessary. It doesn't use try to figure out the types. You can see this in practice here:

class X {
    func x() throws -> X {
        return self
    }
}

let y = try X().x().x()

You only need try one time, even though there are multiple throwing calls in the chain. Imagine how this would work if you'd created overloads on x() based on throws vs non-throws. The answer is "it doesn't matter" because the compiler doesn't care about the try.

Next there's the issue of type inference vs type coercion. This is type inference:

let resul1 = myClass.fun1() // error: ambiguous use of 'fun1()'

Swift will never infer an ambiguous type. This could be Any or it could beInt`, so it gives up.

This is not type inference (the type is known):

let result1: Int = myClass.fun1() // OK

This also has a known, unambiguous type (note no ?):

let x : Any = try myClass.fun1()

But this requires type coercion (much like your print example)

let x : Any = try? myClass.fun1() // Expression implicitly coerced from `Int?` to `Any`
                                  // No calls to throwing function occur within 'try' expression

Why does this call the Int version? try? return an Optional (which is an Any). So Swift has the option here of an expression that returns Int? and coercing that to Any or Any? and coercing that to Any. Swift pretty much always prefers real types to Any (and it properly hates Any?). This is one of the many reasons to avoid Any in your code. It interacts with Optional in bizarre ways. It's arguable that this should be an error instead, but Any is such a squirrelly type that it's very hard to nail down all its corner cases.

So how does this apply to print? The parameter of print is Any, so this is like the let x: Any =... example rather than like the let x =... example.

A few automatic coercions to keep in mind when thinking about these things:

  • Every T can be trivially coerced to T?
  • Every T can be explicitly coerced to Any
  • Every T? can also be explicitly coerce to Any
    • Any can be trivially coerced to Any? (also Any??, Any???, and Any????, etc)
    • Any? (Any??, Any???, etc) can be explicitly coerced to Any
  • Every non-throwing function can be trivially coerced to a throwing version
    • So overloading purely on "throws" is dangerous

So mixing throws/non-throws conversions with Any/Any? conversions, and throwing try? into the mix (which promotes everything into an optional), you've created a perfect storm of confusion.

Obviously you should never, ever write code like this.

like image 85
Rob Napier Avatar answered Jan 31 '23 08:01

Rob Napier