Better init of Result Type with two Optionals

(Hfhbd) #1

Hey,

many async (network) API calls return two optional objects, mostly the returning object and an error, e.g.:

getUser(withID: 42) { (foundUser, error) in 
    if let error = error {
        print(error)
        return
    }
    guard let foundUser = foundUser else {
        print("foundUser is nil")
        return
    }
    doSomething(with: foundUser)
}

Now with Swift 5 we finally have a Result type. This allows to change the handling to this nicer solution:

getUser(withID: 42) { (foundUser, error) in
    switch Result(of: foundUser, or: error) {
    case .failure(let error):
        print(error)
    case .success(let user):
        doSomething(with: user)
    }
}

with this extension:

extension Result {
    init(of success: Success?, or failure: Failure?) {
        if let failure = failure {
            self = .failure(failure)
        } else if let success = success {
            self = .success(success)
        } else {
            fatalError("Neither success \(String(describing: success)) nor failure \(String(describing: failure)) was non nil")
        }
    }
}

Any possibilities to change this extension to remove the fatalError?
And does anybody know if Apple will implement the Result type in their APIs, so this extension could be removed?

(Colin Cashman) #2

Can you elaborate on why you would want to initialize a Result with neither a success nor failure? Are you looking for an indeterminate Result?

#3

He's trying to wrap a call that returns two Optionals by unwrapping them within the init. The reason for the else clause is precisely because he doesn't want an indeterminate result. He's declaring that it's a programmer error by invoking fatalError.

1 Like
(Hfhbd) #4

Exactly. I want only to wrap the Optionals into a Result. With two Optionals, there are four cases:

(User, nil) -> .success(User)
(nil, Error) -> .failure(Error)
(User, Error) -> failure(Error)
(nil, nil) -> ?

Figure, the getUser calls the api of an external service. Mostly there should be an answer, the user, or an error, eg network error or serialization error (of the user data). So Result looks like the perfect type. But (luckily) the compiler forces to handle all cases, including the last case (nil, nil). So, in the end, the last case shouldn't be actually called, but if, yes, this should be a programmer error and the app "should" crash.
Thank you for your answers, I hoped to get a better solution than a crash, but it is fine :smiley:

(Jeremy David Giesbrecht) #5

The fatal error might very well itself be the programmer error, so I am not sure it is a good idea to hide it inside an initializer.

  • An API could be using (nil, nil) to indicate that the networking operations succeed (no error), but the response indicated there is no user (nil user).
  • Or an API might be doing several operations, and returning as much of the intended result it was able to construct alongside the error indicating why it is incomplete.

If an API declares that it returns an optional result and an optional error, all four variations are valid. If they logically are not, the API itself should be adjusted.

If as a user, you assume the result is only either‐or, but cannot prove it, this would be the more accurate, non‐crashing initializer:

extension Result {
    init(of success: Success, or failure: Failure?) {
        if let failure = failure {
            self = .failure(failure)
        } else {
            self = .success(success)
        }
    }
}

But realize that it only makes the usage site even more verbose than it was to begin with:

getUser(withID: 42) { (foundUser, error) in
    switch Result(of: foundUser, or: error) {
    case .failure(let error):
        print(error)
    case .success(let possibleUser):
        if let user = possibleUser {
            doSomething(with: foundUser)
        } else {
            print("no user")
        }
    }
}

vs

getUser(withID: 42) { (foundUser, error) in
    if let error = error {
        print(error)
    } else if let user = foundUser {
        doSomething(with: user)
    } else {
        print("no user")
    }
}
(Jon Shier) #6

I would consider such APIs to be "doing it wrong", as optionals aren't the right way to model either of those situations. The vast majority of double optional completion handlers are simple value / error completion handlers which can be handled like the OP presented. Depending on the level of guarantee given by the API that there will always be either a value or an error, generating a specific error instance when there's a double optional is a possibility. If both values are present, I'm not sure there's a general answer, as that could have a variety of meanings, most specific to the API being used.

2 Likes
(Jeremy David Giesbrecht) #7

Yes. I would also prefer to design the APIs as elaborated below. I understood the question here to be not about how to design an API, but how to simplify the use of an API designed that way by someone else. That other party may well intend either of those semantics (and a lazy vendor might prefer them since the reduced strictness involves less work on his end).


Instead of (nil, nil):

func getAnOptional() -> Result<Something?, Error> { ... }

Instead of both:

func getAsMuchAsPossible() -> Result<Complete, ErrorDetails> { ... }
struct ErrorDetails : Error {
    let underlyingError: Error
    let asMuchAsICouldGetSuccessfully: Partial
}
1 Like
(Davide De Franceschi) #8

Personally I sometimes use this when I convert from Cocoa-style async APIs:

	public init(value: Success?, error: Failure?, allowInconsistentArguments: Bool = false) {
		switch (value, error) {
		case let (value?, error):
			assert(
				error == nil || allowInconsistentArguments,
				"Creating a `Result` with both a value and an error with assertions enabled."
			)
			self = .success(value)
		case let (nil, error?):
			self = .failure(error)
		case (nil, nil):
			assert(
				allowInconsistentArguments,
				"Creating a `Result` with neither a value nor an error with assertions enabled."
			)
			self = .failure(unknownError)
		}
	}
1 Like
(Hfhbd) #9

So, general, your solution is similar, but you can change its behavior by using the bool argument, depending on the API, which is nice, because APIs are different.
Personally, my opinion, every API should send an error or the requested value. If the user is not found, there should be an error, instead returning nil, but not every API is designed this way and a discussion is off topic.