Swift generic constraint on method does not behave as expected when called from instance

Viewed 77

Here is the following piece of Swift code:

class HTTP {
    func run<T: Decodable>(handler: (Result<T, Error>) -> Void) {
        HTTP.handle(handler: handler)
    }
}

extension HTTP {
    static func handle<T: Decodable>(handler: (Result<T, Error>) -> Void) {
        Swift.print("Base")
    }
}

extension HTTP {
    static func handle<T: Decodable & Offline>(handler: (Result<T, Error>) -> Void) {
        Swift.print("Offline")
    }
}

protocol Offline {
    associatedtype Data
    var data: Data { get set }
}

struct Model: Decodable, Offline {
    var data: String = "abc..."
}

let h1 = HTTP()
h1.run { (r: Result<[String], Error>) in }        // 1 - Print "Base" => OK

let h2 = HTTP()
h2.run { (r: Result<Model, Error>) in }           // 2 - Print "Base" => ???

HTTP.handle { (r: Result<[String], Error>) in }   // 3 - Print "Base" => OK

HTTP.handle { (r: Result<Model, Error>) in }      // 4 - Print "Offline" => OK

I am trying to figure out why in case 2, it prints "Base" instead of "Offline". If anyone has suggestions so the right handle method get called depending on the given type T.

I made the handle method static for demo/running purpose to show it works in a static context (case 3 and 4). As you can see, called from the HTTP instance context behaviour is different (case 2).

Any idea ?

2 Answers

It looks like the instance function run(handler:) because it sets as a constraint the T to be only Decodable, filters in some way the Offline constraint. So, passes a type that is only Decodable to the static function handle(handler:) and the compiler assumes that the callable function is the one with only Decodable as a constraint.I do not know if this is understandable.

You can have the expected behavior by adding an overload for your instance function in HTTP class:

class HTTP {
    func run<T: Decodable>(handler: (Result<T, Error>) -> Void) {
        HTTP.handle(handler: handler)
    }
    
    func run<T: Decodable & Offline>(handler: (Result<T, Error>) -> Void) {
        HTTP.handle(handler: handler)
    }
}

The correct HTTP.handle to call is decided at compile time, and baked into the binary. T is only promised to be Decodable in run. It might conform to other protocols, but that's all that's promised. So it compiles this into a call to the Decodable version, even if you call it with something that could be Offline.

This gets to the point of generic specializations. It's not intended to change behavior. It's intended generally to improve performance. For example, consider the following:

func f<Seq: Sequence>(_ seq: Seq) -> Bool { ... }
func f<Seq: RandomAccessCollection>(_ seq: Seq) -> Bool { ... }

Every RandomAccessCollection is a Sequence, so one or the other might be called for an Array. Both should always return the same result. But the second might be more efficient in cases where the system can prove that Seq is Array. If they returned different results, it's not going to work correctly.

Generics are not a way to reinvent class inheritance. If you really need class inheritance, then use classes and inheritance. But generally you should redesign your system to avoid that. How to do that depends on the real goal here.

Related