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

3

5

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: "http://localhost:3000?q=\(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

Cymric answered 19/3, 2021 at 17:42 Comment(0)
T
10

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...
Tiffa answered 19/3, 2021 at 20:54 Comment(0)
C
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()
Cocaine answered 10/11, 2022 at 10:4 Comment(0)
M
0

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()
    }
Mossgrown answered 11/1, 2023 at 12:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.