Tony Lin
Tony Lin

Reputation: 942

why does guard break type inference?

I have met a problem like what title has described: the guard statement breaks type inference for some reason. I have created a Playground project to play around.

Here are some boilerplate code for setup:

import Foundation

struct Model: Decodable {
    var i: String
}

let jsonString = """
{
    "i": "yes"
}
"""

let jsonData = jsonString.data(using: .utf8)
let theModel = try JSONDecoder().decode(Model.self, from: jsonData!)

struct Response<T> {
    var decodedData: T?
}

enum Result<Value> {
    case success(Value)
    case failure
}

struct Client {
    static let shared = Client()
    private init() {}

    func execute<T: Decodable>(completion: (Response<T>) -> ()) {
        let decodedData = try! JSONDecoder().decode(T.self, from: jsonData!)
        completion(Response(decodedData: decodedData))
    }
}

Here's the problem:

struct ServicesA {
    func loadSomething(completion: (Result<Model>) -> ()) {
        Client.shared.execute { result in      // error: generic parameter 'T' could not be inferred
            guard let decodedData = result.decodedData else { return }
            completion(Result.success(decodedData))
        }
    }
}

struct ServicesB {
    func loadSomething(completion: (Result<Model>) -> ()) {
        Client.shared.execute { result in
            completion(Result.success(result.decodedData!))
        }
    }
}

ServicesA breaks whereas ServicesB compiles.

As what you can see, the only difference is guard let decodedData = result.decodedData else { return }. It breaks the type inference so that the Client.shared.execute function complains that the T can't be inferred.

I wonder why would this happen and what's the most appropriate way to deal with this issue.

Upvotes: 2

Views: 197

Answers (4)

Avi
Avi

Reputation: 7552

The compiler only infers closure return types when there is a single statement in the closure.

let x = {
    return 1
}()

                // x == Int(1)

let y = {       // error: unable to infer complex closure return type; add explicit type to disambiguate
    print("hi")
    return 2
}()

https://forums.swift.org/t/problems-with-closure-type-inference/11859/2

Upvotes: 3

Aamir
Aamir

Reputation: 83

TLDR; single line closure compilation is different from multi line

Long answer: Let’s forget single line closures for sometime. When we write generic function accepting a generic as argument type and then call this function, compiler needs to know at time of invocation about what’s the type of function needed i.e. the type of its argument. Now consider this argument is a closure. Again compiler needs to know what’s the type of closure (function signature) at time of invocation. This info should be available in the signature of the closure (i.e. argument and return type), compiler doesn’t worry (and rightly so) about the body of the closure. So Service A is behaving perfectly as expected from compiler i.e. the closure signature doesn’t give any clue of its type.

Now comes in single line closure. Swift compiler has a built in type inference optimisation for single line closures only. When you write single line closure, compiler first kicks in its type inference and try to figure out about the closure including its return type etc from its only single line of body . This type inference kicks in for single line closures (with or without the context of generics). This type inference is the reason that your service B works

So I’ll rephrase the question as “Why does the type inference works for single line closures?” because that’s an extra feature provided by Swift for single line closures only. As soon as you move to multi line closure (its not guard statement specific, same will happen if you put print(“hello world”) as well), this type inference is no more available. Though there may be other type inference checks available for multi line closures.

What you can do is just provide the Type info in closure signature i.e (result: Response< Model >)

Upvotes: 4

Vicente Garcia
Vicente Garcia

Reputation: 6380

Problem is you are referencing data variable but the compiler doesn't know what type is this (because it is a Generic). Try this:

struct ServicesA {
    func loadSomething<T:Model>(completion:(Result<T>) -> ()) {
        Client.shared.execute { result:Response<T> in
            guard let decodedData = result.decodedData else { return }
            completion(Result.success(decodedData))
        }
    }
}

Upvotes: 0

Prashant Tukadiya
Prashant Tukadiya

Reputation: 16446

You need to type cast it result with Result< Model > so can compiler know about T

struct ServicesA {
    func loadSomething(completion:(Result<Model>) -> ()) {
        Client.shared.execute { (result:Response<Model>) in
            guard let data = result.decodedData else {return}
            completion(Result.success(data))
        }
    }
}

Upvotes: 0

Related Questions