Why can't I use .flatMap() after .tryMap() in Swift Combine?

I am studying and trying out a few stuff with Combine to apply on my own and came into the following situation with this contrived example..

let sequencePublisher = [70, 5, 17].publisher
var cancellables = [AnyCancellable]()
sequencePublisher
// .spellOut() .flatMap { query -> URLSession.DataTaskPublisher in return URLSession.shared.dataTaskPublisher(for: URL(string: "(query)")!) } .compactMap { String(data: $0.data, encoding: .utf8) } .sink(receiveCompletion: { completion in switch completion { case .failure(let error): print(error.localizedDescription) default: print("finish") } }) { value in print(value) } .store(in: &cancellables)

I have a sequence publisher that emits 3 Integers and I pass it through flatMap and send a Get request request to my local API that simply returns back the same value it got embedded in a string.

It all works fine, I get all 3 API responses in sink, as long as I don't uncomment the spellOut() custom operator, this operator is supposed to fail if the number is smaller than 6, here is what it does:

enum ConversionError: LocalizedError { case lessThanSix(Int) var errorDescription: String? { switch self { case .lessThanSix(let n): return "could not convert number -> \(n)" } }
}
extension Publisher where Output == Int { func spellOut() -> Publishers.TryMap<Self, String> { tryMap { n -> String in let formatter = NumberFormatter() formatter.numberStyle = .spellOut guard n > 6, let spelledOut = formatter.string(from: n as NSNumber) else { throw ConversionError.lessThanSix(n) } return spelledOut } }
}

The code doesn't even compile if I add another map operator before flatMap it works, but with a tryMap it just says

No exact matches in call to instance method 'flatMap'

Is there any way of achieving this or why is it not allowed?

Thank you in advance for the answers

3 Answers

The problem here is that FlatMap requires the returned publisher created in its closure to have the same Failure type as its upstream (unless upstream has a Never failure).

So, a Sequence publisher, like:

let sequencePublisher = [70, 5, 17].publisher

has a failure type of Never and all works.

But TryMap, which is what .spellOut operator returns, has a failure type of Error, and so it fails, because DataTaskPublisher has a URLError failure type.


A way to fix is to match the error type inside the flatMap:

sequencePublisher .spellOut() .flatMap { query in URLSession.shared.dataTaskPublisher(for: URL(...)) .mapError { $0 as Error } } // etc...
0

You have to map the error after the tryMap.

publisher .tryMap({ id in if let id = id { return id } else { throw MyError.unknown("noId") } }) .mapError { $0 as? MyError ?? MyError.unknown("noId") } .flatMap { id -> AnyPublisher<Model, MyError> in fetchDataUseCase.execute(id: id) } .eraseToAnyPublisher()

In case the error types already match, another point of failure can be, when you work with any Publisher as return values. Then you need to call eraseToAnyPublisher() on the publishers - the first one and the one returned from the flatMap closure.

anyPublisher.eraseToAnyPublisher() .flatMap { value in anotherPublisher.eraseToAnyPublisher() }

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

You Might Also Like