Combine, map() vs tryMap() operators - swift

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

Related

How to differentiate what generic value is what in Swift?

How can I differentiate what generic value is for what in Swift?
For example, what does the value 'T' do and what does the value 'E' do?
func ??<T, E>(result: Result<T, E>, handleError: (E) -> T) -> T {
switch result {
case let .success(value):
return value
case let .failure(error):
return handleError(error)
}
}
what does the value 'T' do and what does the value 'E' do?
They're not "values", they're the names of types — on a par with a term like String or Int. In fact, T could be String or Int. But E has to be some type of Error.
So the phrase <T, E>, which appears twice, just refers to the fact that T and E are generic placeholders for their real types. When someone actually calls this ?? function, the caller will make clear what T and E really are. This is called resolving the generic.
So let's imagine that we call ?? in such a way as to resolve T to String and E to Error. Then in the mind of the compiler, we'll have this:
func ??(result: Result<String, Error>, handleError: (Error) -> String) -> String {
switch result {
case let .success(value):
return value
case let .failure(error):
return handleError(error)
}
}
So now we can read the function declaration. It says: "You hand me two parameters. One, result:, must be a Result enum whose success type is String and whose failure type is Error. The other, handleError:, must be a function that takes an Error and returns a String. And I will return a String to you."
Except, of course, that that is only one way out of an infinite number of ways to resolve T and E. They stand in for the real types that will be resolved a compiled time, depending on how ?? is actually called. So there's your answer; that is "what they do". They are placeholders standing for the real types that will be resolved at compile time.
And in fact, to demonstrate, I will call your function in a way that resolves T to String and E to Error (though I will rename your function myFunc to make the name legal):
func myFunc<T, E>(result: Result<T, E>, handleError: (E) -> T) -> T {
switch result {
case let .success(value):
return value
case let .failure(error):
return handleError(error)
}
}
enum MyError : Error { case oops }
let r = Result<String, Error> { throw MyError.oops }
let output = myFunc(result:r) { err in "Ooops" }
print(output) // Ooops
Footnote: Note that your function cannot really be called ??, as that name is taken. It might have been better to call it foo (the usual nonsense name in these situations).
#matt's answer nails it, so I thought I'd add a like bit of higher level commentary.
Type parameter naming
The use of single characters for these generic type parameters is bit of a legacy a convention from Java and C#, but it need not be so concise. In this example, if you look at the main constraint on the types (Result), note that they use Success and Failure. Using these here would provide a clearer idea about the intent of this function:
func ??<Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success
Thus, a function that takes:
a Result that can either contain a Success or a Failure, and
a closure that takes a Failure and returns a Success
and returns:
a Success
Implementations bourne out of type system constraints
Note that as none of these types are wrapped in Optional the implementation of this function is almost entirely constrained (side-effects notwithstanding).
Take, for example, the simplest function that could seemingly match:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
return Success()
}
Attempting to compile this gives the following error:
main.swift:2:12: error: type 'Success' has no member 'init'
return Success()
^~~~~~~
due to the fact that the Success type is entirely unconstrained in Result, so we don't actually know how to create one inside the function.
We know that Result can contain a Success, so what if we try and forcibly get that?
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
return result.get()
}
This now fails with the following compiler error:
main.swift:2:12: error: call can throw, but it is not marked with 'try' and the error is not handled
return result.get()
^
due to the fact that this function has explicitly denoted that it will not throw, and Result.get() will throw the contained Failure if it does not contain a Success.
The other way to get the Success out of a Result is pattern matching, let's see how that works out matching the enumeration case pattern for a single if:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
if case let .success(success) = result {
return success
}
}
When attempting this, the compilation error is (quite sensibly):
main.swift:5:1: error: missing return in a function expected to return 'Success'
}
^
So we need to also handle the case when a Result contains a Failure. Let's try with another pattern match, using the provided handleError closure:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
if case let .success(success) = result {
return success
}
if case let .failure(failure) = result {
return handleError(failure)
}
}
This still gives the same error as the previous attempt, as the logic could still fall through in this case (this seems unlikely, but this could be subject to a kind of time-of-check to time-of-use bug).
Let's attempt it again, but matching both patterns in a switch:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
switch result {
case let .success(success):
return success
case let .failure(failure):
return handleError(failure)
}
}
There we go, compiling without error.
As is obvious, this is the original function, as provided in your question.
Now that we've arrived back here, can we trim down this implementation? We could try something like:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
switch result {
case let .success(success):
return success
}
}
but this provides this compilation error:
main.swift:2:5: error: switch must be exhaustive
switch result {
^
main.swift:2:5: note: add missing case: '.failure(_)'
switch result {
^
which indicates that we need that .failure(_) case to match all possible outcomes of the result.

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.

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 `Failure` keyword meaning in a Swift Combine chain error

Let's say that I've this simple chain that from an HTTP request creates a publisher for <T, APIManagerError>
func run<T:Decodable>(request:URLRequest)->AnyPublisher<T, APIManagerError>{
return URLSession.shared.dataTaskPublisher(for: request)
.map{$0.data}
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()// it should run mapError before this point
}
This code produces this error since I'm returning Error instead of APIManagerError.
Cannot convert return expression of type
'AnyPublisher<T, Publishers.Decode<Upstream, Output, Coder>.Failure>'
(aka 'AnyPublisher<T, Error>')
to return type 'AnyPublisher<T, RestManagerError>'
I know that to fix the issue I need to add a mapError after .decode.
.mapError{error in
APIManagerError.error("Decode Fail")
}
but I can't really understand what is reported with the error message before the "aka" part that is quite clear instead
How do you read the error Publishers.Decode<Upstream, Output, Coder>.Failure? specifically what does it mean the .Failure part? where can I find the Failure in the Swift Doc?
Since we are talking about two different kinds of errors here lets denote a Compilation Error as CE and the Failure of a Publisher (conforming to Swift.Error) as PF (Publisher Failure).
Your question is about the interpretation of the CE message.
Cannot convert return expression of type
'AnyPublisher<T, Publishers.Decode<Upstream, Output, Coder>.Failure>'
Writes out the resulting returned type of your implementation of func run - without the mapError call. The compiler acknowledges your call to eraseToAnyPublisher() in the end of the function, and also your generic Output of type T. So that covers the Cannot convert return expression of type 'AnyPublisher<T,. As to Publishers.Decode<Upstream, Output, Coder>.Failure>' outputs the derived type of the Failure. This is somewhat a symbolic breakdown of the derived Failure type. Your upstream Publisher is initially of type URLSession.DataTaskPublisher, as a result of your URLSession.shared.dataTaskPublisher call, which you then transform with every Combine operator you call: map and then decode. Resulting in the publisher Publishers.Decode. And the Failure type of cannot be properly "desymbolised" (I lack the proper compiler knowledge to use the correct terminology).
Which Xcode version do you use? The New Diagnostic Architecture might be able to show a better error message. This is in fact the reason why I later in my response use .assertMapError(is: DecodingError.self)
Your code in mapError does the job, but it completely tossed away information about the actual error. So I would not do that. At least print (log) the error. But still I would do something like:
Declare custom Error type
Intuitively we have at least two different kinds of errors, either networking or decoding. But potentially more...
public enum HTTPError: Swift.Error {
indirect case networkingError(NetworkingError)
indirect case decodingError(DecodingError)
}
public extension HTTPError {
enum NetworkingError: Swift.Error {
case urlError(URLError)
case invalidServerResponse(URLResponse)
case invalidServerStatusCode(Int)
}
}
You might need to tell combine that the error type is indeed DecodingError, thus I've declared some fatalError macros useful for this information. It is somewhat simular to Combine's setFailureType (but which only works when the upstream publisher has Failure type Never, thus we cannot use it here).
castOrKill
func typeErasureExpected<T>(
instance incorrectTypeOfThisInstance: Any,
toBe expectedType: T.Type,
_ file: String = #file,
_ line: Int = #line
) -> Never {
let incorrectTypeString = String(describing: Mirror(reflecting: incorrectTypeOfThisInstance).subjectType)
fatalError(
"Incorrect implementation: Expected variable '\(incorrectTypeOfThisInstance)' (type: '\(incorrectTypeString)') to be of type `\(expectedType)`",
file, line
)
}
func castOrKill<T>(
instance anyInstance: Any,
toType: T.Type,
_ file: String = #file,
_ line: Int = #line
) -> T {
guard let instance = anyInstance as? T else {
typeErasureExpected(instance: anyInstance, toBe: T.self, file, line)
}
return instance
}
And then create a convenience method on Publisher, similar to setFailureType:
extension Publisher {
func assertMapError<NewFailure>(is newFailureType: NewFailure.Type) -> AnyPublisher<Output, NewFailure> where NewFailure: Swift.Error {
return self.mapError { castOrKill(instance: $0, toType: NewFailure.self) }.eraseToAnyPublisher()
}
}
Usage:
I took the liberty of catching some more errors in your examples. Asserting e.g. that the server responds with a non failure HTTP status code etc.
func run<Model>(request: URLRequest) -> AnyPublisher<Model, HTTPError> where Model: Decodable {
URLSession.shared
.dataTaskPublisher(for: request)
.mapError { HTTPError.NetworkingError.urlError($0) }
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw HTTPError.NetworkingError.invalidServerResponse(response)
}
guard case 200...299 = httpResponse.statusCode else {
throw HTTPError.NetworkingError.invalidServerStatusCode(httpResponse.statusCode)
}
return data
}
.decode(type: Model.self, decoder: JSONDecoder())
// It's unfortunate that Combine does not pick up that failure type is `DecodingError`
// thus we have to manually tell the Publisher this.
.assertMapError(is: DecodingError.self)
.mapError { HTTPError.decodingError($0) }
.eraseToAnyPublisher()
}
Bonus - equality check for HTTPError
It is indeed very advantageous if our error types are Equatable, it makes writing unit tests so much easier. Either we go the Equatable route, or we can do some reflection magic. I will present both solutions, but the Equatable solution is more robust for sure.
Equatable
In order to make HTTPError conform to Equatable we only need to manually make DecodingError equatable. I've done this with this code:
extension DecodingError: Equatable {
public static func == (lhs: DecodingError, rhs: DecodingError) -> Bool {
switch (lhs, rhs) {
/// `typeMismatch` is an indication that a value of the given type could not
/// be decoded because it did not match the type of what was found in the
/// encoded payload. As associated values, this case contains the attempted
/// type and context for debugging.
case (
.typeMismatch(let lhsType, let lhsContext),
.typeMismatch(let rhsType, let rhsContext)):
return lhsType == rhsType && lhsContext == rhsContext
/// `valueNotFound` is an indication that a non-optional value of the given
/// type was expected, but a null value was found. As associated values,
/// this case contains the attempted type and context for debugging.
case (
.valueNotFound(let lhsType, let lhsContext),
.valueNotFound(let rhsType, let rhsContext)):
return lhsType == rhsType && lhsContext == rhsContext
/// `keyNotFound` is an indication that a keyed decoding container was asked
/// for an entry for the given key, but did not contain one. As associated values,
/// this case contains the attempted key and context for debugging.
case (
.keyNotFound(let lhsKey, let lhsContext),
.keyNotFound(let rhsKey, let rhsContext)):
return lhsKey.stringValue == rhsKey.stringValue && lhsContext == rhsContext
/// `dataCorrupted` is an indication that the data is corrupted or otherwise
/// invalid. As an associated value, this case contains the context for debugging.
case (
.dataCorrupted(let lhsContext),
.dataCorrupted(let rhsContext)):
return lhsContext == rhsContext
default: return false
}
}
}
extension DecodingError.Context: Equatable {
public static func == (lhs: DecodingError.Context, rhs: DecodingError.Context) -> Bool {
return lhs.debugDescription == rhs.debugDescription
}
}
Which, as you can see, also has to make DecodingError.Context Equatable.
Then you can declare these XCTest helpers:
func XCTAssertThrowsSpecificError<ReturnValue, ExpectedError>(
file: StaticString = #file,
line: UInt = #line,
_ codeThatThrows: #autoclosure () throws -> ReturnValue,
_ error: ExpectedError,
_ message: String = ""
) where ExpectedError: Swift.Error & Equatable {
XCTAssertThrowsError(try codeThatThrows(), message, file: file, line: line) { someError in
guard let expectedErrorType = someError as? ExpectedError else {
XCTFail("Expected code to throw error of type: <\(ExpectedError.self)>, but got error: <\(someError)>, of type: <\(type(of: someError))>")
return
}
XCTAssertEqual(expectedErrorType, error, line: line)
}
}
func XCTAssertThrowsSpecificError<ExpectedError>(
_ codeThatThrows: #autoclosure () throws -> Void,
_ error: ExpectedError,
_ message: String = ""
) where ExpectedError: Swift.Error & Equatable {
XCTAssertThrowsError(try codeThatThrows(), message) { someError in
guard let expectedErrorType = someError as? ExpectedError else {
XCTFail("Expected code to throw error of type: <\(ExpectedError.self)>, but got error: <\(someError)>, of type: <\(type(of: someError))>")
return
}
XCTAssertEqual(expectedErrorType, error)
}
}
func XCTAssertThrowsSpecificErrorType<Error>(
_ codeThatThrows: #autoclosure () throws -> Void,
_ errorType: Error.Type,
_ message: String = ""
) where Error: Swift.Error & Equatable {
XCTAssertThrowsError(try codeThatThrows(), message) { someError in
XCTAssertTrue(someError is Error, "Expected code to throw error of type: <\(Error.self)>, but got error: <\(someError)>, of type: <\(type(of: someError))>")
}
}
Reflection magic
Or you can take a look at my Gist here which does not make use of Equatable at all, but can "compare" any enums errors which are not conforming to Equatable.
Usage
Together with CombineExpectation you can now write unit tests of your Combine code and compare errors more easily!
I've easily found the answer actually but I keep the question open in case someone else needed it.
The Failure keyword in this case is just the associatedtype that comes with the Publisher protocol.
In this case I'm just getting the Failure type for the Publishers.Decode, that by default is just Error.

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