Swift flatMap chain unwrap

Viewed 537

Have somebody faced the issue working with such flatMap chain (or even longer) when compiler went to infinite loop.

let what = Future<String, Error>.init { (promise) in
    promise(.success("123"))
}
.flatMap { (inStr) -> AnyPublisher<Int, Error> in
    Future<Int, Error>.init { (promise) in
        promise(.success(Int(inStr)!))
    }.eraseToAnyPublisher()
} 
.flatMap { (inInt) -> AnyPublisher<String, Error> in
    Just(String(inInt))
        .setFailureType(to: Error.self)
        .eraseToAnyPublisher()
}.eraseToAnyPublisher()

The type of Publisher Output and Failure are terrible! enter image description here

I have flatMap chain with 7 steps and you can imagine the actual type. Maybe somebody knows how to handle this correctly? Any help appreciated.

1 Answers

This is really just a mirage. It's exactly equivalent to AnyPublisher<String, Error>:

let what: AnyPublisher<String, Error> = Future<String, Error>() { (promise) in
...

The mirage comes about because of associated types. The final type is Publishers.FlatMap<AnyPublisher<String, Error>, Publishers.FlatMap<AnyPublisher<Int, Error>, Future<String, Error>>>, and from that the AnyPublisher extracts Output and Error. A little bit of unwinding should make it clear that these are just elaborate type aliases for exactly String and Error. Hopefully as Combine becomes more common, the tools will become better at simplifying these type alias in diagnostics. The compiler already does that a lot. It just needs to be a little smarter in these cases. But using an explicit type on the variable (or on the return value of a function), you can get the spelling you want.


Per your comment, that the problem is compile time, that's a common problem with chaining in Swift. It's not Combine-specific and has little to do with the complexity of the types. The most common version of this is having a lot of terms strung together with + (though the Swift team has done a lot of work to improve that one). The solution (as elsewhere) is to break up the expression.

let what = Future<String, Error>.init { (promise) in
    promise(.success("123"))
}

let what1 = what
    .flatMap { (inStr) -> AnyPublisher<Int, Error> in
        Future<Int, Error>.init { (promise) in
            promise(.success(Int(inStr)!))
        }.eraseToAnyPublisher()
    }

let what2 = what1 
    .flatMap { (inInt) -> AnyPublisher<String, Error> in
        Just(String(inInt))
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }.eraseToAnyPublisher()

You don't have to break it up into individual steps like this of course. But at various points, you'll want to break up the expression. Proving that the types are correct can be an exponential-time problem. The way you deal with exponential-time problems is to make sure that "n" is small.

Related