Lucas
Lucas

Reputation: 766

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: "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

Upvotes: 5

Views: 4957

Answers (3)

cybergen
cybergen

Reputation: 3157

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()
    }

Upvotes: 0

Luchi Parejo Alcazar
Luchi Parejo Alcazar

Reputation: 151

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()

Upvotes: 0

New Dev
New Dev

Reputation: 49590

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...

Upvotes: 10

Related Questions