How can I get a value that is derived from Result's success value in the case of success or from an error value in the case of failure, without having to create a temporary variable?
Here's a working but verbose example of what I'm trying to do:
let result = Result<Int, Error>.success(1)
let displayText: String
switch result {
case let .success(value): displayText = String(value)
case let .failure(error): displayText = "Result not available. \(error)"
}
updateDisplayText(displayText)
I could get something close to what by converting the result to an optional value in the desired format, testing it with try operator, and using the nil-coalescing operator to provide the fallback.
updateDisplayText((try? result.map{ v in String(v) }.get()) ?? "Result not available.")
However, that approach doesn't access the failure's error. It also seems overly complex. Is there an approach that works with a single expression, similar to what can be done with an F# match expression?
There are no "match expressions" in Swift. As far as I know, if you want to match against enums with associated values, you must use some kind of statement, whether that's an if case or switch or whatever.
However, no one can stop you from putting that statement into a method, which you can then put inside an extension!
extension Result {
func mapBoth<T>(success: (Success) -> T, error: (Failure) -> T) -> T {
switch self {
case .success(let s):
return success(s)
case .failure(let e):
return error(e)
}
}
}
updateDisplayText can then be called like this:
updateDisplayText(result.mapBoth(
success: { String($0) },
failure: { "Result not available. \($0)" }
))
Related
As follows, what expression should I write to access let learningList? In the code, query is a class, find is a function. Many thanks!
_ = query.find { result in
switch result {
case .success(objects: let learningList):
break
case .failure(error: let error):
print(error)
}
}
That is a local variable, so you can access it only in the decelerated context, i.e. inside case. If you need to use it later you have to create a property in caller class to hold it, like
_ = query.find { [weak self] result in // << ref to caller
switch result {
case .success(objects: let learningList):
self?.learningList = learningList // << safe in caller's property
break
case .failure(error: let error):
print(error)
}
}
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.
I am studying and trying out a few stuff with Combine to apply on my own and came into the following situation with this contrived example..
let sequencePublisher = [70, 5, 17].publisher
var cancellables = [AnyCancellable]()
sequencePublisher
// .spellOut()
.flatMap { query -> URLSession.DataTaskPublisher in
return URLSession.shared.dataTaskPublisher(for: URL(string: "http://localhost:3000?q=\(query)")!)
}
.compactMap { String(data: $0.data, encoding: .utf8) }
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: print("finish")
}
}) { value in
print(value)
}
.store(in: &cancellables)
I have a sequence publisher that emits 3 Integers and I pass it through flatMap and send a Get request request to my local API that simply returns back the same value it got embedded in a string.
It all works fine, I get all 3 API responses in sink, as long as I don't uncomment the spellOut() custom operator, this operator is supposed to fail if the number is smaller than 6, here is what it does:
enum ConversionError: LocalizedError {
case lessThanSix(Int)
var errorDescription: String? {
switch self {
case .lessThanSix(let n):
return "could not convert number -> \(n)"
}
}
}
extension Publisher where Output == Int {
func spellOut() -> Publishers.TryMap<Self, String> {
tryMap { n -> String in
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
guard n > 6, let spelledOut = formatter.string(from: n as NSNumber) else { throw ConversionError.lessThanSix(n) }
return spelledOut
}
}
}
The code doesn't even compile if I add another map operator before flatMap it works, but with a tryMap it just says
No exact matches in call to instance method 'flatMap'
Is there any way of achieving this or why is it not allowed?
Thank you in advance for the answers
The problem here is that FlatMap requires the returned publisher created in its closure to have the same Failure type as its upstream (unless upstream has a Never failure).
So, a Sequence publisher, like:
let sequencePublisher = [70, 5, 17].publisher
has a failure type of Never and all works.
But TryMap, which is what .spellOut operator returns, has a failure type of Error, and so it fails, because DataTaskPublisher has a URLError failure type.
A way to fix is to match the error type inside the flatMap:
sequencePublisher
.spellOut()
.flatMap { query in
URLSession.shared.dataTaskPublisher(for: URL(...))
.mapError { $0 as Error }
}
// etc...
You have to map the error after the tryMap.
publisher
.tryMap({ id in
if let id = id { return id } else { throw MyError.unknown("noId") }
})
.mapError { $0 as? MyError ?? MyError.unknown("noId") }
.flatMap { id -> AnyPublisher<Model, MyError> in
fetchDataUseCase.execute(id: id)
}
.eraseToAnyPublisher()
In case the error types already match, another point of failure can be, when you work with any Publisher as return values. Then you need to call eraseToAnyPublisher() on the publishers - the first one and the one returned from the flatMap closure.
anyPublisher.eraseToAnyPublisher()
.flatMap { value in
anotherPublisher.eraseToAnyPublisher()
}
I am trying to update the value stored in an array property of a class via the use of KeyPaths. Here is my code:
func listenAndUpdateDocuments<T: JSONDecodable>(
_ property: ReferenceWritableKeyPath<MyModel, [T]?>,
from model: MyModel) {
guard let reference = reference else {
return
}
guard listener == nil else {
return
}
listener = backendClient.listenToDocuments(reference) { [weak model] (result: Result<[T], RequestError>) in
switch result {
case .success(let value):
model?[keyPath: property] = value
case .failure:
model?[keyPath: property] = []
}
}
}
The problem is when I call this function like this:
myListener.listenAndUpdateDocuments(\.viewers, from: self)
where viewers is of type [ViewersModel], it always comes back with the following error:
Type of expression is ambiguous without more context
How do I solve this? I have a similar version of the code but where the property parameter isn't an array, and that works.
I struggled with something similar:
_ = Token.query(on: req).filter(\.expiry < Date()).delete()
The solution I found was to use a more up-to-date api to handle my request parameters.
_ = Token.query(on: req).filter(\.expiry, .lessThan, Date()).delete()
It had less to do with the keypath itself than I thought!
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)