【问题标题】:How to successfully match Failure mappings between two Publishers (Never and URLError)如何成功匹配两个发布者之间的失败映射(Never 和 URLError)
【发布时间】:2020-05-04 19:11:16
【问题描述】:

在搜索了一些关于 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 { $0.data }                                            // returns <FlatMap, ?>
    .decode(type: ApiResponse.self, decoder:JSONDecoder())      // returns <ApiResonse, ?>
    .map {$0.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 { $0.data }                                          // 'toGeographiesDataTask()' on 'Publisher'
//        .decode(type: ApiResponse.self, decoder:JSONDecoder())    // requires the types 'Error' and 'Never'
//        .map {$0.body.data(using: .utf8)! }                       // be equivalent"
//        .decode(type: AmznResponse.self, decoder: JSONDecoder())
//        .map { $0.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 返回 但最终因为 DataTaskPublisher 可能失败,我需要将其映射到 URLError 。但是 .mapError 的组合似乎也无法编译。

我在这里遗漏了一些基本的东西吗?似乎是一个容易解决的问题,但我没有发现任何问题。

我已经看到了使用 .flatMap 将它们链接在一起的示例,但是因为我将其中一个的输出转换为第二个自定义发布者的输入,这似乎是不可能的。

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

【问题讨论】:

    标签: combine


    【解决方案1】:

    首先,您有一些可解码的类型,我们需要对其进行模拟才能玩转:

    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: $0) }
                .flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
                .eraseToAnyPublisher()
        }
    }
    

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

    错误:无标题 Page.xcplaygroundpage:31:18:错误:实例方法 'flatMap(maxPublishers:_:)' 需要类型 'Self.Failure' 和 'URLSession.DataTaskPublisher.Failure' (aka 'URLError') 是等价的

                .flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
                 ^
    

    错误:无标题 Page.xcplaygroundpage:32:18:错误:无法转换 返回类型的表达式 'AnyPublisher'(又名 'AnyPublisher') 到 返回类型“AnyPublisher” (aka 'AnyPublisher')

                .eraseToAnyPublisher()
                 ^
    

    Untitled Page.xcplaygroundpage:32:18:注意:泛型的参数 参数“失败”(“Self.Failure”和“错误”)预计为 相等

                .eraseToAnyPublisher()
                 ^
    

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

        func toRegionDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
            let x = self
                .map { apiRequest(for: $0) }
                .eraseToAnyPublisher()
    
            let y = x
                .flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
                .eraseToAnyPublisher()
    
            return y
        }
    

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

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

    错误:无标题 Page.xcplaygroundpage:34:18:错误:实例方法“flatMap(maxPublishers:_:)”需要类型“Self.Failure”和“URLSession.DataTaskPublisher.Failure”(又名“URLError”)等价

    这表示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: $0) }
                .mapError { $0 as Error }
             // ^^^^^^^^^^^^^^^^^^^^^^^^^
                .eraseToAnyPublisher()
    
            let y = x
                .flatMap { URLSession.shared.dataTaskPublisher(for: $0).mapError { $0 as Error } }
                                                                     // ^^^^^^^^^^^^^^^^^^^^^^^^
                .eraseToAnyPublisher()
    
            return y
        }
    }
    

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

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

    我们会给toGeographiesDataTask同样的待遇:

    extension Publisher where Output == [String] {
        func toGeographiesDataTask() -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
            return self
                .map { geographiesRequest(forIds: $0) }
                .mapError { $0 as Error }
                .flatMap { URLSession.shared.dataTaskPublisher(for: $0).mapError { $0 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 { $0.data }
        .decode(type: ApiResponse.self, decoder: JSONDecoder())
        .map { $0.body.data(using: .utf8)! }
        .decode(type: AmazonResponse.self, decoder: JSONDecoder())
        .map { $0.contents }
        .map { $0.map { $0.geoid } }
        .toGeographiesDataTask()
        .map { $0.data }
        .decode(type: ApiResponse.self, decoder: JSONDecoder())
        .map { $0.body.data(using: .utf8)! }
        .decode(type: AmazonResponse.self, decoder: JSONDecoder())
        .map { $0.contents }
        .sink(
            receiveCompletion: { print("completion: \($0)") },
            receiveValue: { print("value: \($0)") })
        .store(in: &tickets)
    
    let region1 = MKCoordinateRegion()
    subject.send(region1)
    

    【讨论】:

    • 这里有很多有用的东西要消化!重构(和演练)非常有帮助,有助于解释 Xcode 更迟钝的错误消息。最直接的影响来自重新拆分代码(最初我正在单独处理链的一部分并且它正在工作),然后使用 eraseToAnyPublisher() 使输出和失败更加可见。这里肯定有一个关于正确开发和重组运营商链的解释器,很多人会发现它很有用。
    【解决方案2】:

    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({ $0 as Error})
                        .eraseToAnyPublisher()
                     })
             .eraseToAnyPublisher()
    }
    

    我认为这应该让链的其余部分也能正常工作,你应该以 &lt;[AmznItem], Error&gt; 作为链末端的 &lt;Output, Failure&gt;

    虽然我还没有在 Playground 中尝试过,但我很确定这应该能让你继续前进。

    【讨论】:

    • 在此之后我得到了这个工作,谢谢 - 当我清理操场时,我想我会分享什么按预期工作和没有按预期工作:剥离“Failure==Error”?谓词?根据 toGeographiesDataTask() 的定义,它没有按预期工作——flatMap 开始抱怨。我确信 mapError 在这里也可能会有所帮助,但是我没有这一步就让它工作了,所以我暂时不理会它。输出和故障的正确映射非常有帮助。希望 Xcode 让这一点更加明显。
    • Followup - 仅剥离Failure 谓词不起作用,因为另一种故障类型被进一步声明到运算符链中。很高兴有多个答案来了解这个过程。
    猜你喜欢
    • 2022-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-30
    • 2015-03-24
    • 1970-01-01
    相关资源
    最近更新 更多