Using Just with flatMap produce Failure mismatch. Combine - swift

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

Related

Swift: execute an operation conditionally on Publisher

I'm using Combine for networking and I want to have some of the combine operations executed only if a certain condition is present, for instance if a local boolean variable needsDecoding is true I want to add .decode(type:,decoder:):
if (needsDecoding) {
return URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} else {
return URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.eraseToAnyPublisher()
}
That code above works but is it possible to conditionally add .decode() to the chain?
Assume for the moment that we try to write a generic function that does the same thing, accepts some data and returns either a generic parsed type or the original data:
func conditionallyDecodeSomeData<T>(data:Data) -> What_Goes_Here? {
...
}
Your question is pretty similar to "What type should this function return". The answer is the type that is the most general superset of all possible results. That's probably Any. (though you want it in a Result monad so it's probably Result<Any, Error>). Using Any would work, but its a pretty bad 'code smell'
If you have a limited number of return types then you could create an enumerated type:
enum OperationResults {
case justData(Data)
case oneParsedResult(Result1)
case anotherParsedResult(AnotherResult)
}
Then your Combine pipeline could produce AnyPublisher<OperationResults, Error>. But if you want something truly generic then you would probably have to use AnyPublisher<Any, Error> which is not satisfying.

Combine sink: ignore receiveValue, only completion is needed

Consider the following code:
CurrentValueSubject<Void, Error>(())
.eraseToAnyPublisher()
.sink { completion in
switch completion {
case .failure(let error):
print(error)
print("FAILURE")
case .finished:
print("SUCCESS")
}
} receiveValue: { value in
// this should be ignored
}
Just by looking at the CurrentValueSubject initializer, it's clear that the value is not needed / doesn't matter.
I'm using this particular publisher to make an asynchronous network request which can either pass or fail.
Since I'm not interested in the value returned from this publisher (there are none), how can I get rid of the receiveValue closure?
Ideally, the call site code should look like this:
CurrentValueSubject<Void, Error>(())
.eraseToAnyPublisher()
.sink { completion in
switch completion {
case .failure(let error):
print(error)
print("FAILURE")
case .finished:
print("SUCCESS ")
}
}
It also might be the case that I should use something different other than AnyPublisher, so feel free to propose / rewrite the API if it fits the purpose better.
The closest solution I was able to find is ignoreOutput, but it still returns a value.
You could declare another sink with just completion:
extension CurrentValueSubject where Output == Void {
func sink(receiveCompletion: #escaping ((Subscribers.Completion<Failure>) -> Void)) -> AnyCancellable {
sink(receiveCompletion: receiveCompletion, receiveValue: {})
}
}
CurrentValueSubject seems a confusing choice, because that will send an initial value (of Void) when you first subscribe to it.
You could make things less ambiguous by using Future, which will send one-and-only-one value, when it's done.
To get around having to receive values you don't care about, you can flip the situation round and use an output type of Result<Void, Error> and a failure type of Never. When processing your network request, you can then fulfil the promise with .failure(error) or .success(()), and deal with it in sink:
let pub = Future<Result<Void, Error>, Never> {
promise in
// Do something asynchronous
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
promise(.success(.success(())))
//or
//promise(.success(.failure(error)))
}
}.eraseToAnyPublisher()
// somewhere else...
pub.sink {
switch $0 {
case .failure(let error):
print("Whoops \(error)")
case .success:
print("Yay")
}
}
You're swapping ugly code at one end of the chain for ugly code at the other, but if that's hidden away behind AnyPublisher and you're concerned with correct usage, that seems the way to go. Consumers can see exactly what to expect from looking at the output and error types, and don't have to deal with them in separate closures.

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

Swift flatMap chain unwrap

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.

Swift 5 Result type

In Swift 5 Apple introduced Result type. It's generic enum with two cases:
public enum Result<Success, Failure: Error> {
case success(Success), failure(Failure)
}
Personally I used to two separate completions in network calls success: Completion and failure: Completion, but from what I see now, Apple pushing us to use single completion with Result type and then inside perform switch. So what are advantages of this approach with Result? Because in a lot of cases I can just omit error handling and don't write this switch. Thanks.
You shouldn’t omit cases when Result is failure. You shouldn’t do it with Result and you shouldn’t do it with your closure for failure. You should handle errors.
Anyway, Result type was introduced for simplifing completion handlers. You can have single closure for handling success or failure (primary-opinion based if two separate closures are better or not). Also Result is designed for error handling. You can simply create your own enum conforming to Error and then you can create your own error cases.
Swift 5 introduced Result<Success, Failure> associated value enumeration[About] It means that your result can be either success or failure with additional info(success result or error object). Good practice is to manage error case and success case as atomic task.
Advantages:
Result is a single/general(generic)/full type which can be used for all yes/no cases
Not optional type
Forces consumers to check all cases
public enum Result<Success, Failure> {
case success(Success)
case failure(Failure)
}
To use it
//create
func foo() -> Result<String, Error> {
//logic
if ok {
return .success("Hello World")
} else {
return .failure(.someError)
}
}
//read
func bar() {
let result = foo()
switch result {
case .success(let someString):
//success logic
case .failure(let error):
//fail logic
}
}