如何成功匹配两个发布者之间的失败映射(Never 和 URLError)

How to successfully match Failure mappings between two Publishers (Never and URLError)

在搜索了一些关于 Combine 的不同资源(包括 Joseph Heck 和 Donny Wals 的书籍)之后,我接近于理解 DataTaskPublishers 的链接,但未能将它们连接在一起成为一系列链接的运算符。我似乎对第一个发布者的输出与第二个发布者的预期输入不匹配这一事实感到困惑。两个 Publisher 扩展都在未连接时工作,所以我确信它缺乏连接两者的能力。我原以为 mapError() 会起作用,但它不想编译。

设置如下:

给定两个自定义发布者:

extension Publisher where Output == MKCoordinateRegion, Failure == URLError {

func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, URLError> {
    return self
        .flatMap({ region -> URLSession.DataTaskPublisher in
                  ...
                  ... 
                  ...
                  return URLSession.shared.dataTaskPublisher(for: request)       
                  })
        .eraseToAnyPublisher()
    }
}

extension Publisher where Output == [String], Failure == Never {

func toGeographiesDataTask() ->  AnyPublisher<URLSession.DataTaskPublisher.Output, URLError {
    return self
        .setFailureType(to: URLError.self)
        .flatMap({ ids -> URLSession.DataTaskPublisher in
                   ...
                   ...
                   ...
                  return URLSession.shared.dataTaskPublisher(for: request)
                 })
         .eraseToAnyPublisher()
}

}

然后我有一个函数试图像这样将两者链接在一起:

   let passthroughSubj = PassthroughSubject<MKCoordinateRegion,URLError>()

    passthroughSubj
    .toRegionDataTask()                                         // returns <DataTaskPublisher, URLError>
    .map { [=12=].data }                                            // returns <FlatMap, ?>
    .decode(type: ApiResponse.self, decoder:JSONDecoder())      // returns <ApiResonse, ?>
    .map {[=12=].body.data(using: .utf8)! }                         // returns <Data, ?>
    .decode(type: AmznResponse.self, decoder: JSONDecoder())    // returns <AmznResponse, ?>
    .map ({ response -> [AmznItem] in                           //
                return response.contents                        // returns <[AmznItem], ?>
    })
    .map ({ items -> [String] in                                // returns <[String], Never> ?
            var ids = [String]()
            for item in items {
                    ids.append(item.geoid)
            }
            return ids
            })
//
//        .toGeographiesDataTask()                                  // get error "Referencing instance method
//        .map { [=12=].data }                                          // 'toGeographiesDataTask()' on 'Publisher'
//        .decode(type: ApiResponse.self, decoder:JSONDecoder())    // requires the types 'Error' and 'Never'
//        .map {[=12=].body.data(using: .utf8)! }                       // be equivalent"
//        .decode(type: AmznResponse.self, decoder: JSONDecoder())
//        .map { [=12=].contents }
//
    .sink(receiveCompletion: { (completion) in
        switch completion {
        case .failure(let error):
            print(error)
        case .finished:
            print("DONE")
        }
        }, receiveValue: { data in
           print(data)
        })
    .store(in: &cancellables)

passthroughSubj.send(region1)

如果我取消对第二个自定义发布者的注释,则会收到右侧显示的错误消息。我的理解是 .map 正在返回 <[String],Never> 但最终因为 DataTaskPublisher 可能会失败,所以我需要将其映射到 URLError。但是 .mapError 的组合似乎也无法编译。

我是不是漏掉了一些基本的东西?似乎是一个很容易解决的问题,但我没有发现任何突出的问题。

我见过使用 .flatMap 将它们链接在一起的示例,但由于我正在将一个输出转换为第二个自定义发布者的输入,这似乎是不可能的。

非常欢迎任何帮助或指点!谢谢

map 运算符仅转换 Output,它使 Error 保持不变。因此,如果我要填写 OutputFailure 对的空白,我最终会得到:

// returns <DataTaskPublisher, URLError>
// returns <Data, URLError>
// returns <ApiResonse, Error> (decode replaces the Failure with Error)
// returns <Data, Error>
// returns <AmznResponse, Error>
// returns <[AmznItem], Error>
// returns <[String], Error>

您对 toGeographiesDataTask 的实施要求应用它的发布商将 Never 作为其错误,这就是您遇到编译器错误的原因。

我认为你可以从你的扩展中删除错误要求并使其成为

extension Publisher where Output == [String] {
  // implementation
}

然后在 toGeographiesDataTask() 内部,您可以使用 mapError:

替换数据任务发出的 URLError
func toGeographiesDataTask() ->  AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
    return self
        .flatMap({ ids -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> in
                   ...
                   ...
                   ...
                  return URLSession.shared.dataTaskPublisher(for: request)
                    .mapError({ [=12=] as Error})
                    .eraseToAnyPublisher()
                 })
         .eraseToAnyPublisher()
}

我认为这应该让链的其余部分也能正常工作,你应该以 <[AmznItem], Error> 作为链末端的 <Output, Failure> 结束。

虽然我还没有在 Playground 中尝试过这个,但我相当确定这应该会让你继续下去。

首先,您有一些可解码类型,我们需要模拟这些类型才能进行游戏:

struct ApiResponse: Decodable {
    var body: String
}

// Does the abbreviation "Amzn" really improve the program?
struct AmazonResponse: Decodable {
    var contents: [AmazonItem]
}

struct AmazonItem: Decodable {
    var geoid: String
}

然后您有几个自定义 Publisher 运算符,每个运算符都需要创建一个 URLRequest。让我们减少嵌套并让 Swift 通过分解出该代码来推断更多类型:

func apiRequest(for region: MKCoordinateRegion) -> URLRequest {
    // Your code here. fatalError gets this through the compiler.
    fatalError()
}

func geographiesRequest(forIds ids: [String]) -> URLRequest {
    // Your code here. fatalError gets this through the compiler.
    fatalError()
}

现在让我们看看您的第一个自定义运算符,toRegionDataTask

  • 您只为 Failure == URLError 的发布商定义了它。也许这就是您真正想要的,但是既然我们要在下游进行解码,并且解码有一个 Failure 类型的 Error,我们就在整个过程中使用 Error

  • 您必须手动指定 flatMap 转换返回的 Publisher 类型。由于我们排除了 apiRequest(for:),我们不再需要这样做。

所以我们可以试试这个:

extension Publisher where Output == MKCoordinateRegion {
    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        return self
            .map { apiRequest(for: [=12=]) }
            .flatMap { URLSession.shared.dataTaskPublisher(for: [=12=]) }
            .eraseToAnyPublisher()
    }
}

但是我们有祸了,因为编译器有抱怨:

error: Untitled Page.xcplaygroundpage:31:18: error: instance method 'flatMap(maxPublishers:_:)' requires the types 'Self.Failure' and 'URLSession.DataTaskPublisher.Failure' (aka 'URLError') be equivalent

            .flatMap { URLSession.shared.dataTaskPublisher(for: [=13=]) }
             ^

error: Untitled Page.xcplaygroundpage:32:18: error: cannot convert return expression of type 'AnyPublisher' (aka 'AnyPublisher<(data: Data, response: URLResponse), Self.Failure>') to return type 'AnyPublisher' (aka 'AnyPublisher<(data: Data, response: URLResponse), Error>')

            .eraseToAnyPublisher()
             ^

Untitled Page.xcplaygroundpage:32:18: note: arguments to generic parameter 'Failure' ('Self.Failure' and 'Error') are expected to be equal

            .eraseToAnyPublisher()
             ^

调试方法是将其分解为多个步骤,并在每个步骤后使用 eraseToAnyPublisher 来查看 OutputFailure 类型:

    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        let x = self
            .map { apiRequest(for: [=16=]) }
            .eraseToAnyPublisher()

        let y = x
            .flatMap { URLSession.shared.dataTaskPublisher(for: [=16=]) }
            .eraseToAnyPublisher()

        return y
    }

现在我们可以看到(通过点击选项x)在map之后,OutputURLRequestFailureSelf.Failure——任何故障类型 self 产生。这是有道理的,因为我从扩展中删除了约束 Failure == URLError

编译器现在只发出第一个先前的抱怨:

error: Untitled Page.xcplaygroundpage:34:18: error: instance method 'flatMap(maxPublishers:_:)' requires the types 'Self.Failure' and 'URLSession.DataTaskPublisher.Failure' (aka 'URLError') be equivalent

这表示 flatMap 运算符的“输入”Failure 类型必须与“输出”Failure 类型相同。输入类型为 Self.Failure,输出为 URLError。这可能就是您在扩展名上限制 Failure == URLError 的原因。但我更喜欢以不同的方式解决它,通过使用 mapError 将两种故障类型转换为 Error。这使得为​​该方法编写测试以及更改它在未来的使用方式变得更加容易。这是我会做的:

extension Publisher where Output == MKCoordinateRegion {
    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        let x = self
            .map { apiRequest(for: [=17=]) }
            .mapError { [=17=] as Error }
         // ^^^^^^^^^^^^^^^^^^^^^^^^^
            .eraseToAnyPublisher()

        let y = x
            .flatMap { URLSession.shared.dataTaskPublisher(for: [=17=]).mapError { [=17=] as Error } }
                                                                 // ^^^^^^^^^^^^^^^^^^^^^^^^
            .eraseToAnyPublisher()

        return y
    }
}

最后我们可以去掉中间步骤得到最终版本:

extension Publisher where Output == MKCoordinateRegion {
    func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        return self
            .map { apiRequest(for: [=18=]) }
            .mapError { [=18=] as Error }
            .flatMap { URLSession.shared.dataTaskPublisher(for: [=18=]).mapError { [=18=] as Error } }
            .eraseToAnyPublisher()
    }
}

我们会给toGeographiesDataTask同样的待遇:

extension Publisher where Output == [String] {
    func toGeographiesDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
        return self
            .map { geographiesRequest(forIds: [=19=]) }
            .mapError { [=19=] as Error }
            .flatMap { URLSession.shared.dataTaskPublisher(for: [=19=]).mapError { [=19=] as Error } }
            .eraseToAnyPublisher()
    }
}

您可能会注意到 toRegionDataTasktoGeographiesDataTask 现在几乎相同。但我将单独留下这个答案。

无论如何,现在让我们看看您的长管道。您收到错误是因为您的 toGeographiesDataTask 具有约束 Failure == Never,但它前面的 map 运算符 而不是 具有 Failure Never 的类型。它具有与其上游相同的 Failure 类型,即 Error (因为 decode(type:decoder:) 运算符)。

自从我从 toGeographiesDataTask 中删除该约束后,管道不再有该错误。我们可以稍微清理一下 geoid 的提取:

// Does the abbeviation "subj" really improve the program?
// The subject's Failure type could be anything here.
let subject = PassthroughSubject<MKCoordinateRegion, Error>()

var tickets: [AnyCancellable] = []

subject
    .toRegionDataTask()
    .map { [=20=].data }
    .decode(type: ApiResponse.self, decoder: JSONDecoder())
    .map { [=20=].body.data(using: .utf8)! }
    .decode(type: AmazonResponse.self, decoder: JSONDecoder())
    .map { [=20=].contents }
    .map { [=20=].map { [=20=].geoid } }
    .toGeographiesDataTask()
    .map { [=20=].data }
    .decode(type: ApiResponse.self, decoder: JSONDecoder())
    .map { [=20=].body.data(using: .utf8)! }
    .decode(type: AmazonResponse.self, decoder: JSONDecoder())
    .map { [=20=].contents }
    .sink(
        receiveCompletion: { print("completion: \([=20=])") },
        receiveValue: { print("value: \([=20=])") })
    .store(in: &tickets)

let region1 = MKCoordinateRegion()
subject.send(region1)