在 flatMap 运算符闭包中放置打印语句后合并奇怪的编译错误
Combine weird compile error after placing a print statement in a flatMap operator closure
我有以下方法(名为:stories
)来自 Ray Wenderlich 的 Combine 书,它从 Hacker News Public API 中获取故事如下:
模型对象:
public struct Story: Codable {
public let id: Int
public let title: String
public let by: String
public let time: TimeInterval
public let url: String
}
extension Story: Comparable {
public static func < (lhs: Story, rhs: Story) -> Bool {
return lhs.time > rhs.time
}
}
extension Story: CustomDebugStringConvertible {
public var debugDescription: String {
return "\n\(title)\nby \(by)\n\(url)\n-----"
}
}
API 结构:
struct API {
enum Error: LocalizedError {
case addressUnreachable(URL)
case invalidResponse
var errorDescription: String? {
switch self {
case .invalidResponse: return "The server responded with garbage."
case .addressUnreachable(let url): return "\(url.absoluteString) is unreachable."
}
}
}
enum EndPoint {
static let baseURL = URL(string: "https://hacker-news.firebaseio.com/v0/")!
case stories
case story(Int)
var url: URL {
switch self {
case .stories:
return EndPoint.baseURL.appendingPathComponent("newstories.json")
case .story(let id):
return EndPoint.baseURL.appendingPathComponent("item/\(id).json")
}
}
}
var maxStories = 10
private let decoder = JSONDecoder()
private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
func story(id: Int) -> AnyPublisher<Story, Error> {
URLSession.shared.dataTaskPublisher(for: EndPoint.story(id).url)
.receive(on: apiQueue)
.map(\.data)
.decode(type: Story.self, decoder: decoder)
.catch{ _ in Empty<Story, Error>() }
.eraseToAnyPublisher()
}
func mergedStories(ids storyIDs: [Int]) -> AnyPublisher<Story, Error> {
let storyIDs = Array(storyIDs.prefix(maxStories))
precondition(!storyIDs.isEmpty)
let initialPublisher = story(id: storyIDs[0])
let remainder = Array(storyIDs.dropFirst())
return remainder.reduce(initialPublisher) { combined, id in //Swift's reduce method
combined
.merge(with: story(id: id))
.eraseToAnyPublisher()
}
}
func stories() -> AnyPublisher<[Story], Error> {
URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
.decode(type: [Int].self, decoder: decoder)
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { ![=11=].isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)") //the print statement that causes the error
return self.mergedStories(ids: storyIDs)
}
.scan([]) { stories, story -> [Story] in
stories + [story] //<--- Error fires here
}
.map { [=11=].sorted() }
.eraseToAnyPublisher()
}
}
消费者代码:
let api = API()
var subscriptions = Set<AnyCancellable>()
api.stories()
.sink(receiveCompletion: { print([=12=]) },
receiveValue: { print([=12=]) })
.store(in: &subscriptions)
该方法无需放入 print("storyIDs are \(storyIDs)")
语句即可完美运行,一旦放置此打印语句,就会在该行触发一个奇怪的编译器错误:stories + [story]
表示:
'[Any]' is not convertible to 'Array<Story>'
我不知道在这种情况下这个误导性错误是什么意思?
Multi-statement closures do not take part in type inference,所以让 flatMap 的闭包成为一个多语句闭包,你会以某种方式导致它错误地推断出 scan
的类型参数。您可以通过在闭包中写出它们来提供它需要的类型:
.flatMap { storyIDs -> AnyPublisher<Story, API.Error> in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}
如果您只想打印收到的值,您也可以调用 .print()
。
更新:
我又试了一下,我发现如果你把 scan
之前的所有东西都放到一个 let
常量中,然后在那个常量上调用 scan
,错误移动到其他地方:
let pub = URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
.decode(type: [Int].self, decoder: decoder) //<--- Error fires here now!
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { ![=11=].isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}
return pub.scan([]) { stories, story -> [Story] in
stories + [story]
}
.map { [=11=].sorted() }
.eraseToAnyPublisher()
Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent
这一次,导致decode
的类型参数被错误推断。 scan
处的原始错误消失了,因为现在编译器知道 pub
具有确定的类型,尽管它在确定 pub
的类型之前(错误地)在其他地方发现了错误。
按照这个模式,我做了另一个临时 let
常量:
let pub1 = URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
let pub2 = pub1
.decode(type: [Int].self, decoder: decoder)
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { ![=12=].isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}
return pub2.scan([]) { stories, story -> [Story] in
stories + [story]
}
.map { [=12=].sorted() }
.eraseToAnyPublisher()
最后,编译器在 storyIDs in
处显示了一条有用的消息,这导致在答案开始时使用解决方案:
Unable to infer complex closure return type; add explicit type to disambiguate
它甚至告诉我们应该插入什么类型!
我有以下方法(名为:stories
)来自 Ray Wenderlich 的 Combine 书,它从 Hacker News Public API 中获取故事如下:
模型对象:
public struct Story: Codable {
public let id: Int
public let title: String
public let by: String
public let time: TimeInterval
public let url: String
}
extension Story: Comparable {
public static func < (lhs: Story, rhs: Story) -> Bool {
return lhs.time > rhs.time
}
}
extension Story: CustomDebugStringConvertible {
public var debugDescription: String {
return "\n\(title)\nby \(by)\n\(url)\n-----"
}
}
API 结构:
struct API {
enum Error: LocalizedError {
case addressUnreachable(URL)
case invalidResponse
var errorDescription: String? {
switch self {
case .invalidResponse: return "The server responded with garbage."
case .addressUnreachable(let url): return "\(url.absoluteString) is unreachable."
}
}
}
enum EndPoint {
static let baseURL = URL(string: "https://hacker-news.firebaseio.com/v0/")!
case stories
case story(Int)
var url: URL {
switch self {
case .stories:
return EndPoint.baseURL.appendingPathComponent("newstories.json")
case .story(let id):
return EndPoint.baseURL.appendingPathComponent("item/\(id).json")
}
}
}
var maxStories = 10
private let decoder = JSONDecoder()
private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
func story(id: Int) -> AnyPublisher<Story, Error> {
URLSession.shared.dataTaskPublisher(for: EndPoint.story(id).url)
.receive(on: apiQueue)
.map(\.data)
.decode(type: Story.self, decoder: decoder)
.catch{ _ in Empty<Story, Error>() }
.eraseToAnyPublisher()
}
func mergedStories(ids storyIDs: [Int]) -> AnyPublisher<Story, Error> {
let storyIDs = Array(storyIDs.prefix(maxStories))
precondition(!storyIDs.isEmpty)
let initialPublisher = story(id: storyIDs[0])
let remainder = Array(storyIDs.dropFirst())
return remainder.reduce(initialPublisher) { combined, id in //Swift's reduce method
combined
.merge(with: story(id: id))
.eraseToAnyPublisher()
}
}
func stories() -> AnyPublisher<[Story], Error> {
URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
.decode(type: [Int].self, decoder: decoder)
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { ![=11=].isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)") //the print statement that causes the error
return self.mergedStories(ids: storyIDs)
}
.scan([]) { stories, story -> [Story] in
stories + [story] //<--- Error fires here
}
.map { [=11=].sorted() }
.eraseToAnyPublisher()
}
}
消费者代码:
let api = API()
var subscriptions = Set<AnyCancellable>()
api.stories()
.sink(receiveCompletion: { print([=12=]) },
receiveValue: { print([=12=]) })
.store(in: &subscriptions)
该方法无需放入 print("storyIDs are \(storyIDs)")
语句即可完美运行,一旦放置此打印语句,就会在该行触发一个奇怪的编译器错误:stories + [story]
表示:
'[Any]' is not convertible to 'Array<Story>'
我不知道在这种情况下这个误导性错误是什么意思?
Multi-statement closures do not take part in type inference,所以让 flatMap 的闭包成为一个多语句闭包,你会以某种方式导致它错误地推断出 scan
的类型参数。您可以通过在闭包中写出它们来提供它需要的类型:
.flatMap { storyIDs -> AnyPublisher<Story, API.Error> in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}
如果您只想打印收到的值,您也可以调用 .print()
。
更新:
我又试了一下,我发现如果你把 scan
之前的所有东西都放到一个 let
常量中,然后在那个常量上调用 scan
,错误移动到其他地方:
let pub = URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
.decode(type: [Int].self, decoder: decoder) //<--- Error fires here now!
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { ![=11=].isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}
return pub.scan([]) { stories, story -> [Story] in
stories + [story]
}
.map { [=11=].sorted() }
.eraseToAnyPublisher()
Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent
这一次,导致decode
的类型参数被错误推断。 scan
处的原始错误消失了,因为现在编译器知道 pub
具有确定的类型,尽管它在确定 pub
的类型之前(错误地)在其他地方发现了错误。
按照这个模式,我做了另一个临时 let
常量:
let pub1 = URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
let pub2 = pub1
.decode(type: [Int].self, decoder: decoder)
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { ![=12=].isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}
return pub2.scan([]) { stories, story -> [Story] in
stories + [story]
}
.map { [=12=].sorted() }
.eraseToAnyPublisher()
最后,编译器在 storyIDs in
处显示了一条有用的消息,这导致在答案开始时使用解决方案:
Unable to infer complex closure return type; add explicit type to disambiguate
它甚至告诉我们应该插入什么类型!