SE-0235 - Add Result to the Standard Library

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.

I would argue no.

In Swift, we have:

  1. thrown application/library Errors
  2. fatalErrors due to programmer error (array index out of bounds, numerical overflow with the appropriate operator), which could be considered contract failures.
  3. fatalErrors due to violating the expected application state (this code should not be reached)
  4. failures due to system state (out of memory error, runtime linkage errors due to things like dynamic loading/JIT)

In Java, unchecked exceptions were meant to handle 2 and 4, through RuntimeException and Error respectively. (They also handle another special case called ThreadDeath, which is a non-handleable error). Unlike swift, it was deemed you may want to recover from these - for instance, because you were running sandboxed code on the thread. Your handler might (for legacy example) kill the Applet, without requiring the Applet Container to be killed/restarted. We have discussed similar possible non-terminating behavior for things like actors. This is considered very useful behavior in server-side swift, where an application is serving requests from multiple users, and termination on programmer error in one part of the application is not considered useful but a potentially exploitable denial-of-service attack.

Developers were meant to use checked exceptions for 1 and 3 in a way similar to Errors in Swift - a function declares that it throws, and you must either catch the error or send it up the stack.

I'd argue that in no case was java originally expecting Result-like semantics or deferred exception handling. Nor does Java have a standard Result type today.

Since Java does not have first-class functions or closures (cue Java 8 argument for the closures one), you typically have a something more akin to a delegate interface, with success and error handlers.

1 Like

I'll reply to my own Java post with a counterpoint :-)

As part of the deficiencies with the current exception handling and the lambda function definitions (which do not allow for checked Exceptions, and unchecked exceptions lead to undefined behavior), I have taken to using Result types to wrap java checked and runtime exceptions into a single type to externalize error handling.

The alternative I've seen is to write or wrap your function so that its error are all runtime errors, and unwrap them on the other side. This seems to be the prevailing strategy, and allows you to do things like terminate early, but the undefined behavior aspect has steered me toward Result.

In this case, it doesn't seem to be part of Java's design, but rather a side effect of them not designing the error model of lambdas and java.util.function at all.

  • What is your evaluation of the proposal?

I’m personally -1 on this.

It’s an opinionated abstraction (Which I use but that’s out of the point) and I don’t feel it makes sense inside the standard library, or in general - inside a programming language in its conception.

I also feel these sort of additions don't even belong inside Swift as a language and committing to a Result type for the lifetime of Swift stdlib feels wrong. Also, if/when proper concurrency is added (async/await hopefully) this type will be even more useless.

It feels like an "alien"-like proposal when compared to other Swift Evolution proposals in my eyes, as it doesn't provide any immense values or enough thought aside from the fact the community has been using this concept as a 3rd party. Someone mentioned antitypical/Result has 100k installations, Do you think that concludes the majority of the community? I hardly think so.

Even if this eventually passes I feel that the proposal as is, is quite semantical but doesn't necessarily solve real problems at this point.

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

I wouldn't say it's significant but it exists. Also I'd say that this isn't really a problem more like an inconvenience, or people's will to not use a 3rd party library for Result/Error abstraction.

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

In my opinion, it doesn't. As I mentioned at the top of my review, something about this proposal feels entirely out of the way to how Swift usually adds proposals into stdlib. Instead of an optimziation or specific helper, it sort of "commits" to a specific way to modeling Results and Errors, which will surely reflect on the future of Swift APIs.

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

I did notice Kotlin and some other languages do have a Result type. I don't have enough in-depth knowledge with it to say if it's similar or solves problems that are already solved in a better way in Swift.

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

A quick reading plus my own usage of general Result types in the OSS community.

3 Likes
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 have my doubts on how to mix Result with synchronous throwing functions.

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

I used Result from the SwiftPM extensively. One thing I feel is missing is AnyError.
Once you have multiple error types that a method can pass to Result, it causes a big developer-time slowdown to translate all these errors to a single type. In its current state, you would end up with a result such as Result<String, Any> or you'd need to take the extra time to translate all these errors to a single error type to have Result<String, MyError> which slows you down.

AnyError really helps speed up development. You make the error type AnyError, e.g. Result<String, AnyError> which allows you to mix and match errors and move forward quickly. Then when time permits, you can replace these errors with a single error to get stronger error handling. AnyError helps a lot with this approach.

Another benefit is that if you have Result<String, AnyError>, you can allow for methods such as mapAny, which is map that accepts throwing functions which throw different errors. Very useful I think. This method can already be found inside the SwiftPM.

Another concern:

Looking at this method:
public init(_ throwing: () throws -> Value)

Doesn't init need to be throwing as well? In case that the error that the closure throws, does not match the error of Result. This is what happens with Result inside the SwiftPM; when the closure throws a different error than what Result states, the initializer throws this mismatched error. This I find the ugly bridging part between Result and throwing functions, because when you pass a throwing function to Result, you still have to try/docatch the initializer of Result. Any ideas on how to handle this cleanly? Note that this isn't a problem when using AnyError because it can accept all errors.

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

A quick reading, but I have been working with SwiftPM's Result almost everyday since its release and wrote a lot about it in my book.

True, but so was do {} while () for many more developers and we still stole that syntax for exception handling and went with repeat{} while() for the former. The precedents are there.

Fair point, but the two are orthogonal really. You can have place for callbacks and proper ones, Rex, Promises, and even better with a nice async/await setup that makes your code looks sane again.

Funnily, I have the exact opposite feeling :-) To me the proposed Result looks like designed by a committee, totally washed out after all expressed needs have been more or less addressed.

I'm concerned that the proposal does not attempt at showing how those needs are addressed. This is concerning because there is a risk we'll need to amend the Result type later, after users have realized the proposed type does not, in practice, meet requirements in terms of features, usability, or extensibility. Extensibility is especially important because we need to address both concrete and non-concrete failure types: users will write extensions. I wish the proposal would explore this area.

I'm also concerned by the fact that the failure type is not constrained to be a Swift Error. First I didn't know that implementing a failure with a non-Error type was even needed. Second this introduces a mismatch between Result and the existing error system. What are the benefits? The drawbacks?

My opinion is that this proposal is incomplete, and can't really be judged in its current form.

5 Likes

Personally I find that these micro-proposals lead to poor design and review fatigue. It makes the design by committee nature of Swift Evolution even worse, because now it is design by multiple committees, as possibly different groups of people design diffrent parts of the solution.

I would like to see Swift Evolution move more towards bigger, more coherent proposals, like the various manifestos, than the current "proposal per git commit" direction that we seem to be going.

12 Likes

I edited my comment to be a fuller review.

IRT to your comment I fully agree. I mean that a Result type conceptually is opinionated and is a single way of dealing with a Result/Error scenario, and I don't feel like it's Swift's responsibility to make that decision.

1 Like