Swift flatMap chain unwrap - swift

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

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

Combine: Casting AnyPublisher<Void, Error> to AnyPublisher<"OurType", Error>

I need to test the code:
func ourFunc() -> AnyPublisher<Void, Never> {
model: OurModel = OurModel()
Just(model).eraseToAnyPublisher()
}
An error occurs:
Cannot convert value of type OurModel to expected type Void
How we can typecast OurModel type to Void?
I don't see any sense in it. Can you explain further?
There's no point in mapping it…
Just(model).map { _ in } .eraseToAnyPublisher()
…because it doesn't matter what the original Output was. It's all going to become ().
Just(()).eraseToAnyPublisher()

Combine, map() vs tryMap() operators

I have been playing with Apple's Combine framework, There I found couple of operators map() & tryMap() or allSatisfy() & tryAllSatisfy
many operators in Combine follows this pattern, I wonder what does this meant for.
I gone through with many operator, most of them have prefixed try. If some one can let me know in most plain manner, that would really helpful.
Thanks
The try variants give the accompanying function the opportunity to throw an error. If the function does throw an error, that error is passed downstream (and the entire pipeline is completed, failing in good order).
So, for example, map takes a function, but you cannot say throw in that function. If you would like to be able to say throw, use tryMap instead. And so on.
The map operator cannot introduce failures. If the upstream Failure type is Never, then after map, the Failure type is still Never. If the upstream Failure type is DecodingError, then after map, the Failure type is still DecodingError:
let up0: AnyPublisher<Int, Never> = ...
let down0: AnyPublisher<String, Never> = up0
// ^^^^^ still Never, like up0
.map { String($0) }
.eraseToAnyPublisher()
let up1: AnyPublisher<Int, DecodingError> = ...
let down1: AnyPublisher<String, DecodingError> = up1
// ^^^^^^^^^^^^^ still DecodingError, like up1
.map { String($0) }
.eraseToAnyPublisher()
The tryMap operator ends the stream with a failure completion if the closure throws an error. A throwing closure can throw any Error. The error type is not restricted. So tryMap always changes the Failure type to Error:
let up2: AnyPublisher<Int, Never> = ...
let down2: AnyPublisher<String, Error> = up2
// ^^^^^ changed from Never to Error
.tryMap { String($0) }
.eraseToAnyPublisher()
let up3: AnyPublisher<Int, DecodingError> = ...
let down3: AnyPublisher<String, Error> = up3
// ^^^^^ changed from DecodingError to Error
.tryMap { String($0) }
.eraseToAnyPublisher()

Mapping Swift Combine Future to another Future

I have a method that returns a Future:
func getItem(id: String) -> Future<MediaItem, Error> {
return Future { promise in
// alamofire async operation
}
}
I want to use it in another method and covert MediaItem to NSImage, which is a synchronous operation. I was hoping to simply do a map or flatMap on the original Future but it creates a long Publisher that I cannot erased to Future<NSImage, Error>.
func getImage(id: String) -> Future<NSImage, Error> {
return getItem(id).map { mediaItem in
// some sync operation to convert mediaItem to NSImage
return convertToNSImage(mediaItem) // this returns NSImage
}
}
I get the following error:
Cannot convert return expression of type 'Publishers.Map<Future<MediaItem, Error>, NSImage>' to return type 'Future<NSImage, Error>'
I tried using flatMap but with a similar error. I can eraseToAnyPublisher but I think that hides the fact that getImage(id: String returns a Future.
I suppose I can wrap the body of getImage in a future but that doesn't seem as clean as chaining and mapping. Any suggestions would be welcome.
You can't use dribs and drabs and bits and pieces from the Combine framework like that. You have to make a pipeline — a publisher, some operators, and a subscriber (which you store so that the pipeline will have a chance to run).
Publisher
|
V
Operator
|
V
Operator
|
V
Subscriber (and store it)
So, here, getItem is a function that produces your Publisher, a Future. So you can say
getItem (...)
.map {...}
( maybe other operators )
.sink {...} (or .assign(...))
.store (...)
Now the future (and the whole pipeline) will run asynchronously and the result will pop out the end of the pipeline and you can do something with it.
Now, of course you can put the Future and the Map together and then stop, vending them so someone else can attach other operators and a subscriber to them. You have now assembled the start of a pipeline and no more. But then its type is not going to be Future; it will be an AnyPublisher<NSImage,Error>. And there's nothing wrong with that!
You can always wrap one future in another. Rather than mapping it as a Publisher, subscribe to its result in the future you want to return.
func mapping(futureToWrap: Future<MediaItem, Error>) -> Future<NSImage, Error> {
var cancellable: AnyCancellable?
return Future<String, Error> { promise in
// cancellable is captured to assure the completion of the wrapped future
cancellable = futureToWrap
.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: { value in
promise(.success(convertToNSImage(mediaItem)))
}
}
}
This could always be generalized to
extension Publisher {
func asFuture() -> Future<Output, Failure> {
var cancellable: AnyCancellable?
return Future<Output, Failure> { promise in
// cancellable is captured to assure the completion of the wrapped future
cancellable = self.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: { value in
promise(.success(value))
}
}
}
}
Note above that if the publisher in question is a class, it will get retained for the lifespan of the closure in the Future returned. Also, as a future, you will only ever get the first value published, after which the future will complete.
Finally, simply erasing to AnyPublisher is just fine. If you want to assure you only get the first value (similar to getting a future's only value), you could just do the following:
getItem(id)
.map(convertToNSImage)
.eraseToAnyPublisher()
.first()
The resulting type, Publishers.First<AnyPublisher<Output, Failure>> is expressive enough to convey that only a single result will ever be received, similar to a Future. You could even define a typealias to that end (though it's probably overkill at that point):
typealias AnyFirst<Output, Failure> = Publishers.First<AnyPublisher<Output, Failure>>

Swift Combine: How can I convert `AnyPublisher<[Foo], *>` to `AnyPublisher<Foo, *>`?

How can I convert a publisher of array a certain element, to just a publisher of said element (but with more events)?
e.g. how can I convert
AnyPublisher<[Int], Never> to AnyPublisher<Int, Never>?
I think maybe what RxSwift offers with its from operator is similar to what I want to do.
I guess I want the inverse of Combine collect?
Here is the code:
func example(publisher: AnyPublisher<[Foo], Never>) -> AnyPublisher<Foo, Never> {
return publisher
.map { $0.publisher }
.switchToLatest()
.eraseToAnyPublisher()
}
What you probably want to do is to use a FlatMap on the publisher of the Foo array, using a function which converts the Foo array to an Observable of Foo (which is where the from comes in).
.flatMap { $0.publisher }

Using Just with flatMap produce Failure mismatch. Combine

I have such code
func request(request: URLRequest) -> AnyPublisher<Data, Error> {
return Just(request)
.flatMap { request in
RequestManager.request(request) // returns AnyPublisher<Data, Error>
}
.eraseToAnyPublisher()
}
and I'm getting compile error:
Instance method flatMap(maxPublishers:_:) requires the types
Just.Failure (aka Never) and Error be equivalent
And it's clear, because Just has Never as Failure and .flatMap requires Error as Failure, so Never != Error
I see 2 approaches:
using right Publisher, instead of Just, but I didn't find good candidate for this.
using some operator like .mapError, .mapError { $0 as Error }, but I'm not sure that it's great idea.
Any ideas how to handle it?
UPDATE:
it makes more sense to use
.setFailureType(to: Error.self)
or
.mapError { $0 as Error }
There is special operator setFailureType(to:). You can override failure type to whatever error type you need.
func request(request: URLRequest) -> AnyPublisher<Data, Error> {
return Just(request)
.setFailureType(to: Error.self)
.flatMap { request in
RequestManager.request(request) // returns AnyPublisher<Data, Error>
}
.eraseToAnyPublisher()
}
https://developer.apple.com/documentation/combine/just/3343941-setfailuretype
If you call .mapError() on the Just output, it will change the type to include Error, but that closure will never be called (so I wouldn’t worry) — this is what I would do unless someone has a better idea.
Normally, the Self.Error == P.Error on flatMap makes sense, as you can’t just ignore errors coming from Self. But, when Self.Error is Never, you can ignore them and produce your own errors.
While the accepted answer certainly works it's pretty verbose. I stumbled on an alternative syntax using Result<Success,Failure).publisher that is somewhat more succinct:
Result.Publisher(.success(request))
(Note that in this case I'm depending on Swift to be able to infer the error type, but you might need to declare it explicitly: Result<URLRequest, Error>.Publisher(.success(request)))