SE-0235 - Add Result to the Standard Library

I agree that this is likely to still be useful — if hopefully lower-visibility — after we add language support for concurrency.

4 Likes

While interesting, this form is limited (the closure allows for multiple statements). In the end I view it as strictly additive, as it would be a large proposal and implementation on its own. And unfortunately that sort of language integration doesn't benefit from the extensive use of Result by the community, so we have little prior art to draw on.

This strikes to the point of my resistance to this. Why are people using Result over try/catch? This proposal solves the problem of there being no Result to draw from, but doesn't solve the underlying cause of needing it in the first place.

1 Like

The error manifesto describes several different types of error handling. try/catch is considered automatically propagating. Also described in the manifesto are manually propagating error systems, which is where Result comes in. Just as having to handle everything manually would be a pain, having everything handled automatically, as it is now, can be very limiting. I think that's where a lot of people's pain comes from. Adding Result removes that limitation.

Another point is not just typed throws (which I don't really care about, but which this proposal leaves a door open to), but having a general mechanism to handle failures that aren't represented (or perhaps can't be, due to performance or other reasons) by Error conforming types. So in this way Result serves as a slightly more primitive but flexible error handling mechanism. I wouldn't expect these uses to be very common, but I think that doesn't mean they aren't there.

1 Like

What is your evaluation of the proposal?

Huge +1.

I'm a maintainer for antitypical/Result. I also maintain several libraries that use that Result type. As an OSS maintainer and a Swift user, I think this would be hugely beneficial.

I like the current names. I've found them to be very straightforward and clear when communicating. Having success/failure and Value/Error have the same length adds nice symmetry to the code.

I've also found that being able to talk about values and successes as different things is helpful when communicating. If this were named case value(Value), then value would be ambiguous, which would hamper communication.

I've never had an issue where Result.Error caused any confusion with people or with the compiler as far as I can remember.

There are a few included APIs that I don't think are necessary:

  • fold()

    antitypical/Result calls this analysis. While I have used this on occasion and found it useful, most people I've worked with have found it confusing.

  • unwrapped()

    I've never found myself wanting this. That might be because of my preferred programming style, which is to mostly stay within Result.

I don't object to their inclusion. But if the desire is to start small, I think these could be removed.

Is the problem being addressed significant enough to warrant a change to Swift?

Yes. There are several popular open source Result types that all compete. I've also worked with a number of open- and closed-source projects that have implemented their own Result out of a desire to have little or no dependencies. That makes interoperability a pain.

While some people clearly don't feel a need for Result or have a desire to use it, there are many of us that do. Anyone who does feels this pain and the community will benefit from a single, authoritative implementation.

Does this proposal fit well with the feel and direction of Swift?

There's a lot of room for interaction between Result and the rest of Swift, as this thread has highlighted. I think this can fit with the feel and direction of Swift. And I don't think Result is going anywhere—people will continue to use it whether or not it's included in the standard library, so it will be part of the feel and direction of Swift.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I've been using and maintaining antitypical/Result since the early days of Swift. I've used it in many open source projects. I've introduced it at work and to many of my colleagues. This proposal is very much in line with that library. I think this covers the necessities of the type.

I haven't spent much time with similar types from other languages, but I've found that my knowledge of Result transfers very easily when reading code or discussing it with people who have used it in other languages.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading of the proposal, skimming of the pitch thread, and years of experience with this API.

5 Likes

I think it's reasonable to see Result and throw as serving different needs within the general realm of error handling and propagation. As I'd mentioned in the previous thread, throw was intentionally designed with an eye toward loosely-coupled, handled-at-a-distance failures, which is why it features automatic propagation (to minimize the overhead of carrying the error to the handler) and no typing (because types would not really help you handle all uncountably many forms of network/file/hardware/etc. error that could accrete through layers of a system). A lot of the people reaching for Result or wishing for typed throws are dealing with more immediate error conditions for which implicit propagation and try/catch ceremony are overkill, and being able to be more specific about the result type is valuable to contain the set of error conditions. It seems to me like it's valuable to have both, and with some investment in language affordances to make Result as fluent to work with as Optional, it could also take pressure off of throws to do more than it currently does.

4 Likes

In my opinion, this is the key part missing from the proposal.

2 Likes

Sure, but alas that's also the part that can be added "at any time", whereas if Result is going to be in the standard library, we'd really like it to be there without ABI versioning constraints.

I also think that the language affordances deserve some longer-term, careful consideration. The affordances we put in place for working with Optional, such as if let and ? chaining, were the best we could come up with given extremely limited time in the pre-1.0 days, but they have clear limitations, and they've gotten in the way of other more general features that could have supplanted them (for instance, pushing pattern-matching conditions into the awkward if case formation). I think it's also true that enum ergonomics in general could use an overhaul; pattern-matching and switching over associated values could be made much easier for all enums, and some of the things you want to do with Result, like bundle up the "error" branches for a chain of Result-producing computations, are things that come up with other enums too.

8 Likes

I think we should care the naming of the similar feature for Future. I personally prefer get like mentioned in Concrete proposal for async semantics in Swift · GitHub . Signatures in the following code have consistency.

extension Result {
    func get() throws -> Value { ... }
}

extension Future {
    func get() async -> Value { ... }
}
2 Likes

This really nice feature. But with the name Result, is this will make the huge migration for current code?

It depends on how pervasive direct use of Result is in your code. If Xcode's rename feature is working, or perhaps just using a find/replace, you should be able to rename your existing type and then migrate at your leisure, adding any convenience functionality you need in an extension.

1 Like
  • What is your evaluation of the proposal?

-1

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes!

  • Does this proposal fit well with the feel and direction of Swift?

I want to say I find the proposal well-written and all the commentary and design work fascinating and informative! It’s been really enjoyable to dig in to.

However, I don’t think this proposal fits well. I share many of the concerns over the details that others have already voiced. In addition, I think this flavor of Result complicates rather than simplifies Swift’s error handling story.

If we imagine a matrix of error handling needs and strategies (sync vs. async, near vs. far, typed vs. untyped, Cocoa interop vs. not, etc.), I feel — as others have already hinted at — this formulation of Result only makes the matrix larger. It provides sync, typed, local errors in a non-Cocoa style only. In addition, because it doesn’t cleanly “bridge”/is not completely isomorphic to the other existing error-handling strategies, it only makes the matrix of error handling we have on offer larger and more murky, particularly for inexperienced programmers.

If we had the sugar to completely unify this and throws, for example, or if we could in some way demonstrate this this was a simpler degenerate case of what will eventually be used for async functions, then I think the argument would be stronger. But as-is, this is just adding another error-ish thing which does not cleanly unify with any of the other error-ish things, which is to be avoided, imo.

We should have something like this! But it should bring clarity with it.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

As an FP partisan (burying the lede here perhaps :laughing:), I would be much more in favor of a less-prescriptive, right-biased Either type, which could certainly be used for errors, but could also be used for any other two-case ADT. There’s a much stronger argument, imo, for the universality of two-case ADTs than there is for one specific flavor of Resultishness.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading, and have lurked some previous threads!

Cheers,
Peter

9 Likes

Reducing duplication in third-party libraries should not be used as the slam-dunk rationale for this proposal while all those same libraries also define a dummy protocol for Result to conform to to allow it to be used in where clauses and so on. Making Error self-conform would erase a major one of those limitations, but it's not the only limitation.

Those obviously aren't and shouldn't be in the scope of this proposal, but it's not valid to say "we should merge this in posthaste because it'll unify the community", because it won't.

6 Likes

I want to +1 this sentiment without quite -1'ing the whole proposal yet. Swift should not cut the Gordian knot by providing two close-but-not-quite features when it could have one. Do we want to add new features through syntax or through types? It's not an either-or choice, but having two full stacks on both sides needlessly bifurcates Swift programmers. It makes the language sprawlingly large and difficult to learn, and that problem won't be helped by magic that somehow freely converts between throws and Result without murdering the poor typechecker.

A hastily-added Result type will inevitably lead to bonkers-looking code like () throws -> Result<Foo, Bar>?. This is not a strawman, I've seen it on my teams. When you're a new dev and the Fix-It tells you to add throws, you add throws.

3 Likes

In this case I think there's a lot of precedent for having both value-oriented and unwinding error handling mechanisms, because they address different use cases:

  • ML has both algebraic data types and untyped exceptions
  • Haskell has both Either and error/catch, and the ecosystem also has many other flavors of exception-like failure handling
  • Rust has both its Result-based error handling model and panic
  • Go has error code-based propagation and panic
  • One could argue Java tries to make this distinction with checked exceptions and implicitly-propagatable runtime errors

More cutting edge languages with effects systems, like Eff and Koka, also generally leave their "exception" effect untyped, from what I've seen.

4 Likes

-1 against this proposal, not against Result as a concept or an eventual standard library feature.

As written, I don't see any reason for this to be part of the standard library:

  1. There is no syntactical additions like with Optional that would justify the compiler knowing about the Result type
    a. There is no ?. syntax for chaining result calls
    b. There are no annotations for converting say a callback function to use result
    c. There is no support for co/contravariance of result (for instance, being able to pass a Result<String,Error> to a function expecting Result<Any, Any>)
    d. If I want to support both result-returning and error-throwing in my API, I have to write multiple methods. This proposal works around that by having the caller initialize a new Result via closure, but if we aren't expecting APIs to consume and return Results, why is it going into the standard library?
  2. Per 1c, having no variance for result types means that people are penalized for not using the most generalized Error type available, by needing to manually flatMap/flatMapError to the type a function expects. This might even lead to people choosing to use an Any type or generics to deal with Results algorithmically.
  3. The case is made to have an Error type to support a possible future typed throws, which means this should wait for a statement saying that typed throws are definitely or definitely not coming
  4. In the sense that we are adding typed error handling before this decision is made, it feels like it will steer us toward typed throws in the future, which I'm against for another big list of reasons.
  5. ..while the case saying this is to support typed throws is then thrown out the window by allowing non-Error types.
  6. There are no changes to optimize API (such as Foundation, for the examples given) to use Result. A proposal IMHO should include a commitment to use Result in the standard/core libraries.
  7. We are not far enough along with concurrency to know how Result would work within that framework, while
  8. The main example here is of a completion callback, which almost certainly is not the concurrency model we would want to go with. Instead, we would want to have an async/await interface, where awaiting on this network function would throw if failed. No result type exposed to the developer at all.
  9. In this sense, there may be a concurrency approach that uses Promises, which may (depending on approaches for async/await code generation) require an untyped or Error-derived error parameter, depending on an eventual "typed-throws" decision. It would IMHO be kinda lousy if the API for Promises and Results were different for reasons which can only be described as "because they were considered at different times", especially with no discernible reason Results need to be added now.

This proposal seems like saying to the community "you can't decide on a single results library to use, so we're throwing one into the standard library". Sure, its handy and will remove some glue code, but it actually supplies nothing new.

I also have concern (but I leave it to the end of this section because I don't have enough knowledge to justify it, and don't want to spread FUD) that encouraging people to use Result when not needed will possibly result in de-optimized code, and will almost certainly result in more compiler work in trying to inline processing or error handling.

Possibly; I'm honestly not quite sure what the problem is that is being addressed, as there is nothing in this proposal that isn't already be implemented in a third party library.

There are problems that could be addressed, such as language-level integration or use in standard libraries, which would warrant having Result as it is then a dependency. This proposal has no such timelines or even recommendations.

Yes and no, in the sense that this isn't meant to align with any future direction. The decision for having a typed, non-rooted error could create tons of headaches in the future, especially if in the future a decision is made that typed errors aren't worth the baggage, and/or that typed errors should only be advisory (say, a documentation feature). Even more so without language support for covariant casting.

Likewise, there are plenty of concurrency decisions that could be made which would deal with a promise or future type with quite a different surface area than a standard result. Seeing as there aren't any features above and beyond a third party Result, why push it out earlier?

Not many to be honest, generally it has been via Promises and ACTs. Completion Tokens within a completion callback wind up looking like results, and I appreciated the equivalent of the proposed unwrapped method to allow me to use regular exception handling rather than decomposing success vs error.

Generally within synchronous code (and even asynchronous callbacks), my experience has been that you don't defer error handling by storing a result - you put your objects into an error state. In a simplistic case this might mean that your data model is a single result, but for me it has typically meant that you roll the error state into one more more states within your state machine (say: loading, validating, completed(data), failure(error) )

In-depth study, as well as plenty of arguing in previous discussions :slight_smile:

17 Likes

Sure - but for evaluating this proposal, it doesn't feel like it is a language feature. The language itself is not proposed to use this type at all, rather just take some set of the trade-offs various third-party result types have made, and make it first party.

In other words, there isn't enough substance to this proposal to say it is useful, and it doesn't fit into any long-term concurrency manifesto or additions to the swift style guidelines for callbacks, or so on that I know of. It's just settling the dispute over which GitHub repo people should get Result from.

I don't understand the ABI versioning constraints - as defined, this would be a net new Swift.Result<T,E> type with zero language or other API integration. Why would this need to be added in Swift 5.0 vs 5.0.1 (e.g., any point in the future)?

Then why aren't we doing those first? If we've rushed decisions in the past and regretted them, why would we add Result now when it's 100% capable of existing in this described form within a third party library? Why risk painting the language into a corner?

5 Likes

Swift evolution governs both the language and standard library. Not every standard library feature needs to directly impact the language.

ABI versioning cuts both directions. Swift 5.0 will be the first ABI-stable version of Swift shipping in vendor OSes. If we add Result later, it will not be available on platforms shipped with older Swift runtimes, so developers would not be able to use it without requiring their users to run a newer OS.

The fact that so many projects use existing Result packages or roll their own is indication that it's desirable to a large number of developers. As you said yourself, this proposal doesn't impact the language at all directly, so it's unlikely to "paint the language into a corner" considering it.

5 Likes

I disagree to add Result to the standard library for now.

Major use cases of Result are error handling of asynchronous operations. We will be able to do it without Result when Swift supports async/await.

// without `Result` nor `async/await`
func download(from url: URL, _ completion: (Data?, Error?) -> Void)

// with `Result`
func download(from url: URL, _ completion: Result<Data, Error> -> Void)

// with `async/await`
func download(from url: URL) async throws -> Data

Even when async/await is supported, I think we will still need Result to combine with Future for concurrent asynchronous operations if Future does not support error handling. However, I think Result by a simple implementation like below would be enough for those purposes.

enum Result<Value> {
    case success(Value), failure(Error)
    func get() throws -> Value { ... }
}

// assuming that parameterized extensions are supported
extension<T> Future where Value == Result<T> {
    init(_ body: () async throws -> Void) { ... }
}

// concurrent asynchronous operations
let futureData1: Future<Result<Data>> = .init { try await download(from: url1) }
let futureData2: Future<Result<Data>> = .init { try await download(from: url2) }

// instead of `get().get()`, single `get()` with `async throws` can be implemented
// for `Future<Result<_>>` in the extension above
let data1: Data = try await data1.get().get()
let data2: Data = try await data2.get().get()
foo(data1, data2)

I think what we want about Result becomes clear after we start using async/await and Future for Swift. In addition, we have not concluded if we need typed throws, which affect if we want the second type parameter of Result. Because Result is purely additive, we have no reason to add Result to the standard library now. So I think it is too early to discuss Result for the present.

10 Likes

Few types have the special sort of handling Optional is given by the compiler, and the first versions of Optional, way back when, weren't that special either. As with Optional, they can be added, over time, as the points of friction are found. As for b and c, even if I had a design and implementation ready to go, those features would be separate proposals, as they affect the Obj-C overlay and the core compiler respectively. No proposal touches all of those parts at the same time. With d, I'm not sure why you'd want to do that, as it seems completely unnecessary, but even if you did, the proposal includes API to turn a result in to a throwing function, so it would be trivial to bridge the two. Even if that were a motivating case (I don't believe such a pattern is common in the community), it would again be a separate proposal to create some sort of implicit conversion between the two systems.

As a generic type, dealing with Result generically is appropriate. In any case, using the mapping operations is expected, as, even if the language supported the variances completely, you would still need explicit conversion between types for types which aren't related.

Typed errors were indeed mentioned as one reason behind having a generic Error type, but it wasn't the only or indeed the most motivating factor. Being able to represent failures outside of Swift.Error is powerful in many ways on its own, and that's the biggest factor behind a generic, yet unconstrained Error.

Even if I had such designs ready to go, they would have no place in this proposal. At this point, as outlined in the proposal, Result would fill the manually propagated error role as outlined in the error manifesto, unlocking use cases not well handled currently, and opening the way for future integration, like markup that can convert Obj-C blocks to Result closures.

We don't know, but as I outline in earlier posts, Result can stand on its own outside of async usage. In fact, more of the proposal is spent outlining those uses than the async one. It seems likely to me, however, that Result, with have some role to play, perhaps alongside a manually propagating async form like a Promise.

Unifying around a single Result representation is indeed a motivating factor, but I want to point out that Result does indeed provide something new to the language: manually propagated error handing. While use of this case is not going to be as popular as the automatic try/throw, it has uses that that construct can't handle.

In my experience, a Result can be an important part of your state, especially if you have multiple states that have success or failure representations.