Can't set AsyncStream `onTermination` handler - swift

Has anyone succeeded in creating an AsyncStream and setting its onTermination handler? I can't do it. The following is copied and pasted directly from the proposal (https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md), except I got rid of the warnings by modernizing detach into Task.detached:
let t = Task.detached {
func make123Stream() -> AsyncStream<Int> {
AsyncStream { continuation in
continuation.onTermination = { termination in
switch termination {
case .finished:
print("Regular finish")
case .cancelled:
print("Cancellation")
}
}
Task.detached {
for n in 1...3 {
continuation.yield(n)
sleep(2)
}
continuation.finish()
}
}
}
for await n in make123Stream() {
print("for-in: \(n)")
}
print("After")
}
sleep(3)
t.cancel()
Looks great, but it doesn't compile, and I can't find a way to make it compile. The error message on the onTermination setter reads:
Converting non-concurrent function value to
'#Sendable (AsyncStream<Int>.Continuation.Termination) -> Void'
may introduce data races
I don't know what the compiler is asking me to do. Has anyone worked this out, and what's the solution?
(I've filed a bug on this.)

Update:
You can work around this bug by adding #Sendable as the first thing inside the closure (before the capture list, parameters, and the in keyword), like:
continuation.onTermination = { #Sendable termination in
switch termination {
case .finished:
print("Regular finish")
case .cancelled:
print("Cancellation")
#unknown default:
break
}
}
Original answer:
Yeah I'm guessing it's a bug, because I was able to get it to compile by adding:
as (#Sendable (AsyncStream<Int>.Continuation.Termination) -> Void)
after the closure, like:
continuation.onTermination = { termination in
switch termination {
case .finished:
print("Regular finish")
case .cancelled:
print("Cancellation")
#unknown default:
break
}
} as (#Sendable (AsyncStream<Int>.Continuation.Termination) -> Void)
(I also added the #unknown default case to silence a new warning that appeared.)

Related

Swift Combine framework setFailureType error operator

For scientific reasons I've created a Publisher and a Subscriber so I can dive into Combine.
The Publisher has been converted from a never failing to the failing one.
enum IntegerError: String, Error {
case miltupleOf2 = "We are sorry but the number is a multiple of 2, therefore cannot be used in the process"
}
let integerPublisher = [1,3,3,3,3,3,5,6,7,7].publisher
.setFailureType(to: IntegerError.self)
let subscribtion = integerPublisher
.tryMap { intValue in
if intValue.isMultiple(of: 2) {
throw IntegerError.miltupleOf2
} else {
return intValue
}
}
.sink { completion in
switch completion {
case .finished:
print("success")
case .failure(let error):
if let error = error as? IntegerError {
print(error.rawValue)
} else {
print(error)
}
}
} receiveValue: { value in
print(value)
}
My question is: when using sink, the error type is Error. Why is it not the custom IntegerError that I've used within the .setFailureType modifier?
The need of casting my error to the type that I specified earlier seems a little redundant.
Thank you.
The reason for this is quite straightforward. tryMap returns a Publishers.TryMap<Upstream, Output>, which is a specific kind of publisher with Failure == Error:
typealias Failure = Error
So as soon as you use tryMap, that undoes what setFailureType did.
The reason why Publishers.TryMap has Error as its Failure type is because in Swift, you can't specify what specific type of error a closure can throw (unlike Java for example). Once you mark a closure as throws, like tryMap has done with its transform parameter:
func tryMap<T>(_ transform: #escaping (Self.Output) throws -> T) -> Publishers.TryMap<Self, T>
any Error can be thrown inside the transform closure. You can try changing throw IntegerError.miltupleOf2 to throwing another type of error. Your code will still compile.
Hypothetically, if Swift allowed you to specify what type of error you are allowed to throw in the closure, then tryMap could have been declared as (fake syntax):
func tryMap<T, E: Error>(_ transform: #escaping (Self.Output) throws E -> T) -> Publishers.TryMap<Self, T, E>
and you wouldn't even need setFailureType.
As a workaround, you can use mapError to cast the error to your desired type:
let subscribtion = integerPublisher
.tryMap { intValue -> Int in
if intValue.isMultiple(of: 2) {
throw IntegerError.miltupleOf2
} else {
return intValue
}
}.mapError { $0 as! IntegerError }
.sink { completion in
switch completion {
case .finished:
print("success")
case .failure(let error):
print(error.rawValue)
}
} receiveValue: { value in
print(value)
}
You need to reassure the compiler that you haven't thrown any other type of errors in tryMap.

ReactiveSwift pipeline flatMap body transform not executed

I have the following pipeline setup, and for some reason I can't understand, the second flatMap is skipped:
func letsDoThis() -> SignalProducer<(), MyError> {
let logError: (MyError) -> Void = { error in
print("Error: \(error); \((error as NSError).userInfo)")
}
return upload(uploads) // returns: SignalProducer<Signal<(), MyError>.Event, Never>
.collect() // SignalProducer<[Signal<(), MyError>.Event], Never>
.flatMap(.merge, { [uploadContext] values -> SignalProducer<[Signal<(), MyError>.Event], MyError> in
return context.saveSignal() // SignalProducer<(), NSError>
.map { values } // SignalProducer<[Signal<(), MyError>.Event], NSError>
.mapError { MyError.saveFailed(error: $0) } // SignalProducer<[Signal<(), MyError>.Event], MyError>
})
.flatMap(.merge, { values -> SignalProducer<(), MyError> in
if let error = values.first(where: { $0.error != nil })?.error {
return SignalProducer(error: error)
} else {
return SignalProducer(value: ())
}
})
.on(failed: logError)
}
See the transformations/signatures starting with the upload method.
When I say skipped I mean even if I add breakpoints or log statements, they are not executed.
Any idea how to debug this or how to fix?
Thanks.
EDIT: it is most likely has something to do with the map withing the first flatMap, but not sure how to fix it yet.
See this link.
EDIT 2: versions
- ReactiveCocoa (10.1.0):
- ReactiveObjC (3.1.1)
- ReactiveObjCBridge (6.0.0):
- ReactiveSwift (6.1.0)
EDIT 3: I found the problem which was due to my method saveSignal sending sendCompleted.
extension NSManagedObjectContext {
func saveSignal() -> SignalProducer<(), NSError> {
return SignalProducer { observer, disposable in
self.perform {
do {
try self.save()
observer.sendCompleted()
}
catch {
observer.send(error: error as NSError)
}
}
}
}
Sending completed make sense, so I can't change that. Any way to change the flatMap to still do what I intended to do?
I think the reason your second flatMap is never executed is that saveSignal never sends a value; it just finishes with a completed event or an error event. That means map will never be called, and no values will ever be passed to your second flatMap. You can fix it by doing something like this:
context.saveSignal()
.mapError { MyError.saveFailed(error: $0) }
.then(SignalProducer(value: values))
Instead of using map (which does nothing because there are no values to map), you just create a new producer that sends the values after saveSignal completes successfully.

What to put in switch cases that have nothing to do?

I started using Result as a return type and I mostly like it but when I have nothing to return for success then I am at a loss about what to do in that case statement. Any hints?
All that I could think of was let _ = 0
func createAppDirectory(_ searchPath: FileManager.SearchPathDirectory) -> Result<Void,Error>
...
switch createAppDirectory(searchPath) {
case .success(_): let _ = 0
case .failure(let error): return .failure(error)
}
I am beginning to think that maybe Result isn't a good fit when the success type is Void.
BTW createAppDirectory just creates Library/Application Support/<Bundle ID>. There is no value to return if it succeeds.
Use a break statement:
switch createAppDirectory(searchPath) {
case .success:
break
case .failure(let error): return .failure(error)
}
EDIT:
As Mojtaba pointed out, if you're not going to use the associated value for a particular case of your enum you can simply skip it. I've edited my answer above to remove the (_) from the .success case
Just ignore it:
case .success: break
Also if you want absolutely no overwork when it isn't failure case, gaurd it at the very beginning of the scope:
guard case .failure(let error) = createAppDirectory(searchPath) else { return <#Value#> }
If only the error is significant Result is inappropriate.
A better pattern is to throw the error and return nothing
func createAppDirectory(_ searchPath: FileManager.SearchPathDirectory) throws
...
do {
try createAppDirectory(searchPath)
} catch { print(error)}
Return simple void result,
switch createAppDirectory(searchPath) {
case .success: return .success(())
case .failure(let error): return .failure(error)
}

Specifying custom error type within the Result type in Swift 5

I am trying to create a Result variable with a custom error type with the builtin Result type in Foundation for Swift 5, but I can't quite get the type system to understand the kind of error I want to throw.
The code below does not compile.
import Foundation
enum CustomError: String, Error {
case somethingBadHappened
}
struct Model {
let value: Int
}
class Request {
func execute(number: Int, completion: #escaping (Result<Model, CustomError>) -> Void) {
let result = Result { () throws -> Model in
if (number < 20) {
throw CustomError.somethingBadHappened
} else {
return Model(value: number)
}
}
// compiler complains here about: Cannot convert value of type 'Result<Model, Error>' to expected argument type 'Result<Model, CustomError>'
completion(result)
}
}
let request = Request()
request.execute(number: 19) { result in
switch result {
case .success(let value): print("Succeded with \(value)")
case .failure(let error): print("Failed with \(error)")
}
}
Changing the signature of the completion closure to completion: #escaping (Result<Model, Error>) -> Void works, but then I am not using the custom error type.
How can I make the type system understand I would like to use the custom error type?
Apologies for giving a second answer, but there needs to be a corrective for fphilipe's answer.
You can use init(catching:) to form the Result and yet return it as a Result<Model, CustomError>. That is what mapError is for! Like this:
enum CustomError: String, Error {
case somethingBadHappened
}
struct Model {
let value: Int
}
class Request {
func execute(number: Int, completion: #escaping (Result<Model, CustomError>) -> Void) {
let result = Result { () throws -> Model in
if (number < 20) {
throw NSError()
} else {
return Model(value: number)
}
}.mapError { err in
return CustomError.somethingBadHappened
}
completion(result)
}
}
I have to throw something in order to form the initial Result<Model, Error>, so I just throw an NSError as a kind of placeholder. But then mapError comes along and transforms this into a Result<Model, CustomError>. The power of mapError is that it changes only what happens in case of failure.
Thus we are able to keep the original form of the code.
Changing the signature of the completion closure to completion: #escaping (Result<Model, Error>) -> Void works, but then I am not using the custom error type.
Yes, you are! Change the signature in exactly that way, so that you compile, and then run your code. When we get to this line:
case .failure(let error): print("Failed with \(error)")
... we print "Failed with somethingBadHappened". That proves that your CustomError.somethingBadHappened instance came through just fine.
If the problem is that you want to separate out your CustomError explicitly, then separate it out explicitly as you catch it:
case .failure(let error as CustomError): print(error)
default : fatalError("oops, got some other error")
Or if you want to winnow it down still further and catch only the .somethingBadHappened case, specify that:
case .failure(CustomError.somethingBadHappened): print("Something bad happened")
default : fatalError("oops, got some other error")
Those examples are artificial, but they demonstrate what they are intended to demonstrate — that your CustomError instance is coming through with full integrity.
Just create Result manually:
let result: Result<Model, CustomError>
if (number < 20) {
result = .failure(.somethingBadHappened)
} else {
result = .success(Model(value: number))
}
completion(result)

RxSwift catch networking and reachability errors

I try to use retryOnBecomesReachable method from the RX example files in my networking layer
extension ObservableConvertibleType {
func retryOnBecomesReachable(_ valueOnFailure:E, reachabilityService: ReachabilityService?) -> Observable<E> {
return self.asObservable()
.catchError { (e) -> Observable<E> in
return reachabilityService.reachability
.skip(1)
.filter { $0.reachable }
.flatMap({ _ -> Observable<E> in
return Observable.error(e)
})
.startWith(valueOnFailure)
}
.retry()
}
}
// My layer
request
.flatMapLatest{ request in
provider.rx.request(request)
.map{ User.self }
.map{ RequestState.loaded($0) }
.retryOnBecomesReachable(.error(.notConnectedToInternet), reachabilityService: reachabilityService)
.catchError({ .just(.error($0)) })
.startWith(.startLoading)
}
Without this method, all works awesome. All error catching and returning .just(.error($0)) sequence.
With this method, the retry feature works awesome. But when something happens (mapping, decoding or other error) I get .notConnectedToInternet. I think the reason in .startWith(valueOnFailure) method. I tried to move, remove, change position but nothing helps. I'm stuck.
What should I do to use retry feature and catch errors correct?
I think that basically changing .startWith(valueOnFailure) to startWith(e) might work for you. Another option is to check if the error is a reachability error to begin with inside the catch block.
e.g.
.catchError { e in
guard e == SomeError.notConnectedToInternet else {
return .error(e)
}
... rest of your code