Adding Result to the Standard Library


(Jean-Daniel) #53

While the concept is interesting, I don’t like the name. It is not descriptive and confusing, as Swift closures are not Block (Block is an Obj-C term).
They are not even compatible with block, unless explicitly requested (by applying custom calling convention).


(Rod Brown) #54

Agreed about the naming, which is of little consequence. We use this too.

I think the bigger issue here is Result<T> had a lot of pushback. I think the fact it’s used so ubiquitously in other libraries makes it valuable enough to include in a shared swift “Extended Library” ala Boost but at this stage I’m not sure the Core Team have much interest in implementing something like this that they won’t use in the Standard Library…


(Chris Lattner) #55

I personally think it is time to standardize Result, and I believe that several other core team members feel the same way. The major problem with doing so is that is blocked by an unresolved disagreement (in both the core team and community): whether Swift should support typed errors.

If we add typed throws, then I believe that the design is clear: throws would allow at most one type to be specified, and if none is specified, Error is the default:

func foo() throws {
func bar() throws Error {  // same as above
func baz() throws MyEnum {   // ok

While many people will argue for a list of possible thrown things, there are lots of reasons not to do that, not least of which is that you can represent such a list of options with an enum.

Whether we add this or not ends up impacting Result. In a world without typed throws, we should define result as Result. If we have typed throws, we should have Result<NormalResultType, TheErrorType>.

On this core disagreement, there are lots of reasonable arguments on both sides, but it would be healthy to start hashing those out (in its own thread). We don’t need to actually implement typed throws to get result, we just need a decision about whether we should implement it one day. Not making that decision continues the sad state of affairs where we don’t have a standardized Result type.

-Chris


(David Waite) #56

(Making sure typed throws goes into a different, probably ridiculously long thread - assuming in any examples here that it is just Result)

IMO the thing which cements Result in the standard library is language and compiler support for Result comparable to Optional. Most of the features in the Swift namespace are there because they are features that involve language syntax (optional, literal types, error) and/or because they are needed for language-level C/ObjC interoperability (array, string, C vararg, unsafe memory access, data). Even stream I/O isn’t in the standard library.

What would I think compiler support look like? A random assortment of potential features would be:

  • Automatic promotion of a T to Result<T>.
  • terse try syntax for unwrapping result to a value or error, possible custom syntax for case expressions.
  • Optional chaining style usage to deal with Result holding an error
  • Functional equivalence/compatibility of styles. A developer should not have to use wrappers to have a single function definition or closure usable as (for instance):
    • 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
  • Possible rethrows-style allowances, where generic algorithms and the compiler can preserve the fact that a type will not represent an error.
  • Special compiler optimizations of Result (such as avoiding creation of intermediate type instances when unnecessary, or removing unnecessary code knowing that a particular code path cannot return a failure, under the premise the optimizer wouldn’t already do these).

(Erica Sadun) #57

I’d like to circle back to the notion of unwrappable that you designed way back if we’re going to have a formal Result type.


(Joe Groff) #58

I don’t think Result needs to be tied to error handling. It’s useful to be able to interoperate with throws, but a two-argument Result would be useful for clients regardless, and maximally future-proof. We could constrain throws interop APIs to E == Error for now, and reserve the right to generalize them to E: Error if the language ever supports typed throws.


(Lukas Stabe 🙃) #59

Rust has a Try-trait that may be worth looking at, if I’m understanding correctly what you are talking about.


(Jon Shier) #60

@dwaite

Automatic promotion of a T to Result<T>.

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

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.

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?

Functional equivalence/compatibility of styles. A developer should not have to use wrappers to have a single function definition or closure usable as (for instance):

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.

Possible rethrows-style allowances, where generic algorithms and the compiler can preserve the fact that a type will not represent an error.

We can start with whatever the compiler already does with its invariant analysis and go from there. I’d guess Optional has similar cases.

Special compiler optimizations of Result (such as avoiding creation of intermediate type instances when unnecessary, or removing unnecessary code knowing that a particular code path cannot return a failure, under the premise the optimizer wouldn’t already do these).

Sure. This may be similar to other optimizations around enums.

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.


(Jon Shier) #61

The ergonomics of such a type are rather poor, since it would require the creation of some sort of AnyError type to wrap every instance an API throws just an Error. Given that all current Apple APIs only throw Error, and that even Swift-native APIs will throw Error, this seems like an anti pattern (unless we want to promote users encapsulating every error in their program into a single type).

In order for the type to really make sense as the manually propagating alternative to throws, I think it needs to match the underlying error philosophy of the language. Typed throws changes that slightly, which is why I think most people want to have a decision there before adding Result. And despite the fact that Result could be used outside of throw, that is one of the primary use cases, so minimizing the friction between throws and Result seems like a good thing.


(Xiaodi Wu) #62

Error has no Self or associated type requirements (or any requirements), so AnyError shouldn’t be necessary, right?


(Jon Shier) #63

I just meant that, since an Error instance can’t be used in a Result<Value, Error> instance, you’d need to wrap it in some strong type. It’s not a type erasure box, but a strong type box.


(Xiaodi Wu) #64

Ah, well the poor interaction between Result<T, U: Error> and untyped throws really boils down to that whole debate about the pros and cons of having typed throws in the first place, doesn’t it? If I understand you, you’re coming down on the side of Result<T>.


(Jon Shier) #65

Right.

I wish I could edit my original post in this thread and correct the link, but the PR is here. I haven’t been updating it much since the typed throws discussion seems like a prerequisite.


(Joe Groff) #66

AnyError shouldn’t be necessary. Error already works as a self-conforming existential type because it has no contravariant requirements, as well as a specialized representation that avoids the representational issues with other self-conforming existentials.


(Jon Shier) #67

I’m not sure what you mean, attempting to declare a Result<Data, Error> fails with the usual “Using Error as a concrete type conforming to protocol Error is not supported.” error that we’ve seen for years.

Edit: Rereading your initial response, it sounds like you’re talking about Either<T, E> instead of a typical Result. Either has approximately zero usage in the wild, so it seems like it’s unnecessary.


(Joe Groff) #68

Hm, I thought the runtime supported this. Making the Error type conform to the Error protocol should be easier than the general case regardless.

(e) I am definitely talking about Result. I don’t think it necessarily needs a constraint on the ‘left’ side to communicate the bias toward the success case.


(Jon Shier) #69

Perhaps it would be useful, for me at least, for you to declare your Result type, if it’s any different from Result<T> or Result<T, E: Error>.


(Joe Groff) #70

Something like this:

enum Result<T, E> {
  case success(T), failure(E)

  func map<U>(_: (T) -> U) -> Result<U, E>
  func flatMap<U>(_: (T) -> Result<U, E>) -> Result<U, E>
  /* etc. */
}

extension Result where E == Error {
  init(_: () throws -> T)
  func get() throws -> T
}

// add `extension Result where E: Error` in a future Swift with typed throws

E can have an Error constraint, but it doesn’t necessarily need one to be useful.


(Jon Shier) #71

That essentially Either, but with the extensions, could replicate current Result<T> functionality. Cases make the use case more clear than the left and right of Either too. Interesting. I can try an integration into Alamofire and see if it still works well.


(Adrian Zubarev) #72

I know this is bikeshedding but coming from the RxSwift world I’d think that those map and flatMap operators wouldn’t be flexible in practice since E is fixed and cannot be mapped. Here is the RxSwift design rationale not to use generic error.


Here is also a blog post from John Sundell where he shows an example of Future and Promise types, but which requires a non-generic Result type: