Adding Result to the Standard Library

I think your observation that a Result type with typed errors can be paired with a typealias to provide a Result “type” with untyped errors. However, I think your solution is heavily biased towards use of Result (untyped errors) rather than ResultBase (typed errors). We could just as easily haveResult<T, E> and something like ResultWithUntypedErrors so I think it’s quite a stretch to say that the proposed design makes both camps happy.

The core issue is that a decision needs to be made as to what the name Result will mean. If we typed errors are chosen people will be free to add a typealias if they wish. You are proposing that punting on this by calling the foundational ResultBase and supporting both models in the standard library is better than making a choice and living with it. My instinct is that this could cause confusion and will not fully satisfy those who prefer typed errors. It feels to me like a direction that could end up in a similar stat as the endless access control debate.

The topic of typed errors has been discussed in quite some detail in the past. I am not sure there are any new arguments to be made on this topic, but perhaps a new debate focused on the higher-level question of whether we should have typed errors or not (rather than the design details of a specific proposal for typed errors) could help focus and clarify the positions of both sides.

IMO, there is not going to be a clear winner here and the core team is going to have to make a judgement call to settle the discussion. I would love to see that happen soon even if the design and implementation of typed throws (if that is the direction) doesn’t happen immediately. Getting a Result type that is aligned with the intended direction of the langue into the standard library would be greatly beneficial to everyone.

One additional observation I would like to contribute is that there are a lot of people using a Result type with typed errors already. I haven’t seen any discussion of substance about how this would be addressed if we introduced a Result type with untyped errors into the standard library. Would people be willing to spend effort to migrate code and give up typed errors (which they may prefer) in order to adopt the standard library type? If not, how large will this segment of the Swift community be and what are the consequences of this split? We should consider this very carefully.

At a glance, this looks like a very nice approach.

3 Likes

Yes, I too like Joe's AnyResult, CocoaResult, etc.

Those names avoid any obvious bias.

I'd like to know if the "untyped result camp" can see AnyResult, or AnyErrorResult as an acceptable compromise:

// The base Result type
enum Result<T, E> {
    case success(T), failure(E)
}

// For the untyped camp:
typealias AnyErrorResult<T> = Result<T, Error>

If since Error does not (yet) comply to itself, would the "typed result camp" be happy the other type alias below?

// For the typed camp:
typealias ErrorResult<T, E: Error> = Result<T, E>

Thoughts?

I also wonder if if would be a good idea to have the base result type have an awful name, so that both camps could define the plain Result to their preference:

// Stdlib defines ResultBase
enum ResultBase <T, E> {
    case success(T), failure(E)
}

// UntypedResult.swift
typealias Result<T> = ResultBase<T, Error>

// TypedResult.swift
typealias Result<T, E: Error> = ResultBase<T, E>

Of course, only ResultBase would ship in the stdlib.

Being explicitly an opinionated language, Swift should ship with one or the other, not both. A decision needs to be taken on support for typed throws or not.

Is there a place where the arguments for and against each side are documented?

I can empathize with you and Chris's desire to answer that question, but I worry that predicating acceptance of Result on answering the typed throws question is tying a boat anchor to a glider. Result seems fairly uncontroversially good, whereas typed throws is a much hotter topic.

edit: Also, although Result and throws are both tools that address parts of the general "error handling" problem, I think they still represent distinct philosophies or aspects of that same problem. The design of throws is geared toward distant, general, loosely-coupled error handling, much like exceptions in other languages, and its untypedness makes sense in that context. Result, being generally the immediate return value of a function, is better suited to immediate, tightly-coupled error handling, and so specificity for the "left" side of the error seems desirable there independent of the question of typed throws.

1 Like

Despite my attempts to show how a well-designed base result type plus nice type aliases can fit both models? If you have overlooked them, please have a second read.

I haven't overlooked them. It's not about whether it's possible or not to have both models; the point is that an opinionated language, being opinionated, presents only one model.

The ultimate opinion would be not to have Result at all, which has been the language's historic position, with an eye toward async and other future features defining away the most common use cases for it. Many people have cast reasonable doubt on whether async coroutines would truly define away the desire for Result, and without even more complex type-system level features like effect polymorphism or higher-kinded types, etc., Result will be a necessary abstraction for tunneling failable computations through other abstractions. Those type system features will only show up in quite a few years from now, if ever, and perfect tomorrow is the enemy of good today.

4 Likes

I was writing up my post below and then this popped up, so fortuitous timing!

The following might be an unfounded fear, but one thing that concerns me is whether a Result type would result (...) in splitting APIs into two "camps": those that convey errors using throws and those that convey them using Result.

I'm very fond of the fact that Swift uses completely different flow control by default for error outcomes vs. expected results, and I don't like the way that Result conflates the two. If I look at a function that uses try or do-catch, it's obvious from the call site what's going on with regard to error handling. If I see let x = somethingThatReturnsResult(), type inference has removes any context from the call site and I have to know that that function returns Result or infer it from how x is used later in the function.

That could be resolved somewhat by having the compiler treat Result as a known type with special conversions, similar to the syntactic sugar we have for Optional.

Imagine a throwing function; right now it would be called like so:

func someComputation() throws -> Int { ... }

let x = try someComputation()

What if you were allowed to drop the try if the expression was being used in a Result<Int> context instead?

// `try` not needed here because the compiler will wrap it for us
let x: Result<Int> = someComputation()
// could possibly support this too if we wanted
let x = someComputation() as Result<Int>

The reverse transform could be added as well—a function that returns a Result could be used in a try expression, which would unwrap the result or rethrow the error.

This would all be similar to the initializers and methods that folks have already added to their result types that assist with this kind of bridging, but I think making it first class in the language would go a long way toward giving Result the weight needed to add it to the standard library. It would also set the stage for future transforms around the async/await use cases that you mention above.

2 Likes

Do you have any supporting evidence for this claim? My experience has been almost the opposite, that swift is very inclusive. The whole swift evolution process is basically about not being opinionated. I mean, till Swift 3 you could assign tasks and micromanage the swift developers by writing a proposal and shouting +1 / -1.

If the E: Error constraint is important, making the Error type conform to the Error protocol is something we could implement. The Error type’s special one-word representation and the existing bridging logic that has to unwrap nested error values in order to preserve NSError identity should make this easier than the general case of protocol type self-conformance.

This seems like the best approach to me for a number of reasons:

  1. The existing Error limitation seems arbitrary and can be frustrating apart from Result

  2. Semantically it makes sense that the failure case, which results in an error, would have a value that conforms to Error.

  3. Adding where E: Error quickly becomes cumbersome. It's often not clear where you'll need, so IME you often end up needing it somewhere and have to add it in a bunch of places where it was missing before.

Adding Result that behaved like this, with:

  1. Typed throws that default to Error
  2. Typed Results with errors that can be just Error
  3. Bridging between throws and Results so that Result is a reified try result

Would make me very happy. This would result in a lot more compatibility between 3rd-party libraries. (And be less code that I'd need to maintain, which is always a good thing.)

2 Likes

Shouting +1 and -1 was explicitly discouraged--and still is. The evolution process is not a democratic process, and one major purpose of core team review is to ensure that the result fits into a single coherent vision:

From the use I’ve seen of Result in other frameworks, it’s generally only been used where the control-flow based error handling can’t work at this time, eg asynchronous work. Even then, by having throwing unwrap methods, it often is used simply as an encapsulation method to hold the error or result, and delay control flow until a context where it can be used, rather than remove it entirely.

I don’t think this really encourages divergence in and of itself. I guess there is a risk of it when async await gets in, but async await would be cleaner rather than closures and result values anyway so I think the language itself will promote the preferred method.

The cases where async await will not work in the current proposals eg URLSessionTask cancellation, will need something like this anyway to clean up an otherwise cumbersome and error-prone API.

3 Likes

I imagine the URLSession API will need a revisit in general. Even the completionHandler methods won't work with async/await.

1 Like

Yes, this is what the proposal and the Error Manifesto refer to as manual error propagation. It was one of several axis examined in the manifesto and allows some use cases that the current automatic propagation can't handle or can't handle well, even outside of asynchronous behavior. Some of these are outlined in the proposal. I imagine there will be something similar for future async behavior, where async gives the automatic propagation of asynchronous behavior and something else the manual version.

2 Likes

Automatic promotion of a T to Result.

And also promotion of an E to a .failure(E), right?

I don't know if that could be handled unambiguously, since there is no restriction of T to not be an E.

Plus at some point you get to wanting a feature like:

  extension Result {  
      public static from<T>(_ function: () throws -> T) -> Result<T> {  
          do {
              return .success(try function());  
          }  
          catch {  
              self = .failure(error);  
          }  
      }  
 }  

let r = Result.from { ... }

terse try syntax for unwrapping result to a value or error, possible custom syntax for case expressions.

I’m not sure exactly what you want, but the proposed Result has unwrap(), so you can do let value = try result.unwrap()`. This could be integrated with other unwrapping proposals as well.

I'm proposing potential features warranting Result to be in the Swift package. With Optional, you can do if let x = x. Are there such language syntax features for result?

Optional chaining style usage to deal with Result holding an error

I can’t imaging what this would look like, can you post an example?

let retrievedName:Result<String> = retrieveRecord(id)?.person?.combineFirstAndLastName()

let profile:UIImage = loadProfileData(localPath)!.profileImage

where both retrieveRecord and loadProfileData return Result

func foo<X,T>(arg:X, callback:(T?, Error?)->())
func foo<X,T>(arg:X, callback:(Result<T>)->())
async func foo<X,T>(arg:X) throws -> T

I’m not sure automatic conversions from callback: (T?, E?) -> Void should be supported in Swift, but I can imagine some sort of Objective-C markup to allow such a conversion to happen when APIs are exposed to Swift. However, I certainly envision Result being able to be used as the manual propagation of whatever async features we get in the future, similar to how it can be thought of as the manual propagation of throws right now.

IMHO, simply making a Result type in the core library solves enough of the functional composition problems. Sure, I won't have to adapt a function from one Framework returning Result#1 so that it can be used with a Framework expecting Result#2, but I will need to manually adapt functions which deal with result types to and from those which deal with direct return values and errors.

These are all nice things, but they’re all additive, as far as I can see, and shouldn’t impact the initial introduction of the type.

I'd argue that if IO doesn't belong in core Swift, Result might not belong there either. Perhaps we propose Result in Foundation if there is no advantage of it being in the Swift package?

Sure there is an aspiration to be opinionated, but I'm talking about what happens in practice. Swift evolution is all about finding consensus, there was even a poll recently in one post. Opinionated means being ok with making some people unhappy.

Disclaimer: I wish swift was more opinionated, but we need to face reality.

Proposal authors might want to find consensus in their designs (which is their choice), but not finding consensus in no way decides whether a proposal can get submitted for review or getting accepted.