在 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

它甚至告诉我们应该插入什么类型!