Having Result's Failure type conform to Swift.Error is needlessly restrictive

I can sympathize, but I still feel that your situation is specialized, and that the semantic meaning of a Result type is diminished if the failure isn't an Error. The standard library is opinionated, but most of all it's standard. If your use-case isn't standard, you need to look beyond the standard lib.

The standard library's definition cannot represent one of the most commonly-used framework-provided callbacks (the aforementioned URLSession.dataTask(with:completion:) callback). I'd call that use-case pretty darn standard.

The standard library is supposed to serve the standard needs of the community, not define them. Opinionated is good when it guides people away from bad patterns. It's bad when it forces people into using bad patterns in order to accommodate the standard library (which in this case means defining meaningless wrapper types that only serve to obscure the real error).

It's perfectly fine to say "you're strongly encouraged to make your Failure type conform to Error" and to conditionally add functionality to Result that only takes effect with this constraint (e.g. the get() method). That would be reasonable. But as it stands, the standard library has decided to define a Result type that cannot even be used consistently across Apple's own frameworks, let alone being used by third parties.

If I have a computation that can succeed or fail, I should be able to represent that success or failure with a Result. It's really that simple. I can be encouraged to play nicely with Error, but whether or not I actually do that is my decision.

This is an aside but I agree with this very much and have been thinking about how these could be used for a while now. I wonder if it would be worth starting a thread about anonymous structs...

1 Like

This was brought up in the original text for SE-0235 itself with a similar argument, but it's actually mistaken: this API only results in an Error on an underlying network failure, which precludes receiving a URLResponse. I was informed by the API maintainers that the correct type would be something like Result<(Data, URLResponse), Error>, which is why that's what's presented in the proposal.

9 Likes

Your URLSession example is incorrect. This was a motivating example in the proposal and the simplest form is merely Result<(URLResponse, Data), Error>, as URLSession does no response validation. If you want such validation you can easily transform the initial Result into something else. But yes, you'll need your own error type at that point, but that's no different than using throws.

There is no bad pattern here. At very worst you're forced to create a type to encapsulate all of the error information you want to convey. If you disagree with that requirement I suggest you review the error handling rationale that informed the original throws design as well as the Result proposal.

This really doesn't make any sense. Result can be used easily in this way and was used by the community for years before being added to the language, with none of the popular Result types allowing unconstrained errors. Personally, I've ported private and public projects to use the new type, including Alamofire, and it works just fine.

Your issue is larger than Result: Swift as a language has decided that computations that can fail and want to convey more than success or failure must do so by using an Error conforming type. That's how throws works and that's how Result works. If you find that requirement too onerous, you can easily write your own handling.

This is incorrect. Result, as it exists in Swift and many other languages, is specifically an error handling mechanism. It's intentionally different from throws in both form and capability. That it's not as sugary as possible is rather irrelevant to the fact that, not only was the original, unconstrained proposal designed for error handling, but it became even more so when refined by the core team after the first review, which added the Error requirement. The benefit of the requirement was outlined in that first review: it reinforces Swift's decision to have all error handling flow through Error and enhances the semantic meaning of the type, beyond the success / failure cases and Success / Failure generic types.

Please do. There’s a rich area of functionality to explore between tuples (anonymous structs), union types (anonymous enums), and newtype.

1 Like

I probably don’t know the full extend of your scenario—you do best in that regard—so correct me if I’m wrong.

From what I gather sourceText is an accompanying value that is outside of Error. So I figure that it should also exist outside of the Result. In that case, shouldn’t it instead be

(sourceText: String, result: Result<Model, Error>)

2 Likes

The documentation explicitly states that if a URLResponse was received prior to the error occurring that it would be provided to the callback. Are you saying the documentation is wrong? Because what it describes makese sense; if a network error occurs after I've received the response but before I've received all of the data, it seems reasonable to supply the URLResponse to my callback.

Every result type I've used, for years, has had an unconstrained failure type on the result.

In fact, if a previous community-defined Result type required the failure to conform to Error that would mean that you could not use Error itself as the failure type. Are you telling me that you've been using community-supplied Result types that cannot represent Result<T,Error>? I haven't seen one of those myself and I find it hard to believe that this type was particularly usable. I wager the result type you were working with just looked like Result<T> instead.

I'm getting really frustrated at going around in circles here.

The idea of a result type is not intrinsically tied to the existing error-handling scheme. It's great for a result type to interoperate with this, yes. Interoperating with this does not mean that Result needs to define its Failure type as conforming to Error, it only means that the interoperation exists under that condition.

Incorrect. Computations that can fail where the failure is not an Error type are free to use a 2-variant enum with associated values.

Which is to say, they're free to use a Result type.

All existing instances of "computation that can fail but the error is not an Error" must clearly already be represented by a type along these lines. It makes no sense at all for the standard library to try and say "stop defining these exact same enums everywhere, we'll just do it once in the standard library" but then intentionally choose to not actually provide a type that satisfies the need.

I've said it before, and I will say it again: there is no benefit to this restriction on Result. None whatsoever. All this restriction does is force me to obscure my code and make error handling harder than it should be. It actively hurts my code and there's no reason why it has to do this.

Computation results and error handling are two separate but related concepts. Please stop lumping them together. It's this insistence on treating Result and throws as the same thing that leads toward insisting that the result Failure type must conform to Error.

It does not enhance the semantic meaning of the type. It just forces me to bend my code into contortions to satisfy its irrational demands. It literally hides the semantics of my failure case. I already have an error as part of my failure data. I'm already meeting the spirit of the requirement. But the actual fact of the requirement actively hurts my code.


There are good reasons why throws works with Error specifically and not arbitrary data. Beyond the philosophical argument, the fact that throws is propagating a weakly-typed value means that the requirement to conform to Error gives the caller certain expectations as to the behavior of this untyped value, some constraints on what to type-check it against in order to extract more data, and also the ability to cast to NSError and get something usable.

These reasons do not apply to Result. Result has a strongly-typed Failure value that doesn't require type-checking, doesn't need to set expectations on the caller for how to try and convert it back into something usable, and doesn't demand interoperability with Obj-C (e.g. if my function returns Result I cannot mark it @objc). In fact, if I want to have a function that throws an error but also returns additional data, I cannot use throws, and returning an enum like Result is precisely how I'm supposed to handle that case. Which makes it pretty damn laughable that I cannot, in fact, use Result for this case.

1 Like

I believe the documentation is referring to success or failure at the level of the application protocol, e.g. a 20x vs. 40x HTTP status code; it is not trying to say that the API will return a URLResponse as long as it can parse such a response before any transport-level failure occurs. I'm sure it could be clearer about that.

I am not an expert in this API, but I'm getting this information from someone who is, so I would be surprised if it's off-base.

The most common implementation of Result is antitypical/Result. Here's what the type looked like 4 years ago: https://github.com/antitypical/Result/blob/9c0ab35e641ad336075e743942dbf22b77e22345/Result/Result.swift

Non-2xx status codes do not result in an error. URLSession only gives an error for network-level issues. Here's what the documentation has to say about the response parameter:

If the request fails, the data parameter is nil and the error parameter contain information about the failure. If a response from the server is received, regardless of whether the request completes successfully or fails, the response parameter contains that information.

I don't know how to read this other than "if we get a response object, we'll give it to you, even if the request subsequently fails while receiving the body". If this is not in fact how it's implemented, then I would consider that a bug given how explicit the documentation is about the behavior.

That Result type does not have a bound on the failure. It named the failure type Error, but it's actually just a generic type parameter and is unrelated to Swift.Error.

You are correct. Here's the commit from Aug 2, 2016: https://github.com/antitypical/Result/blob/a285bbc03874cfdf000b7b905e428c491fd497cd/Result/Result.swift

The words “while receiving the body” do not appear in the documentation. “Regardless of whether the request succeeds or fails” is referring to application-level success or failure. I believe the documentation is attempting to tell you that application-level failure does not produce an Error, which some users might expect.

You’re arguing by proxy with the maintainers of the API here. If you want to file a bug asking for better documentation, please do so, but you’re not going to convince me that your interpretation is correct, because it’s not.

3 Likes

Nowhere else in the documentation is application-level failure even defined. In that exact same paragraph, it uses the phrase "If the request fails" to refer to network-level failure, and "If the request completes successfully" to refer to the request completing without network-level failure regardless of status code.

If the implementation is not even attempting to behave this way, then this is not a case of "better documentation", this is a case of "the documentation very explicitly disagrees with the implementation", and given that the documentation defines the API contract, this means the implementation has a bug.

In any case, what the implementation actually does is irrelevant for the purposes of this conversation. The fact is, it's eminently reasonable to believe that a network-level failure after having received the HTTP headers would still supply the URLResponse object, and therefore it's reasonable to want an API of this nature to provide to its callback a Result<(URLResponse, Data>, (URLResponse?, Error)>.

I don't think this conversation is going anywhere, but I'll leave the thread open.

2 Likes

You seem to be a highly experienced engineer. Without having seen your code I am curious as to why a wrapper type would obscure the actual error. To be honest it looks from the outside like no “practical” solution will be enough for you- you want the design changed “or else”. If that’s the case you can stop reading here: I highly doubt that will happen (given source stability) and to be honest I was personally against the proposal to add a Result type in any form whatsoever, so I don’t really care. But if you’re willing to work with what we’ve got:

Are you saying the wrapped Error type can vary but all wrapped errors have an associated sourceText? I think I understand you’re parsing text or something similar.

My first thought would be you could define an enum for your various Errors and use an associated value for sourceText. If there’s a lot different errors I could see how that would be tedious to set up though (and potentially error-prone, code-wise). I do imagine that this might have better optimisation potential than a two-member struct (or even the tuple), but that’s just my layman’s view. It also depends on the rest of your code.

Another suggestion in this thread was to consider moving sourceText to a Tuple wrapping the result type (sourceText, Result). In my mind that would make just as much sense, if you’re processing lots of chunks of text and are able to gather results for each chunk. But again, haven’t seen the code.

As somebody who didn’t want a Result type at all, I feel your frustration at a design that doesn’t make sense to you. Maybe there is some greater good in democracy, though, if we look.

I do want the design changed. I think it's doable; Swift tries for source stability but it's not a requirement, and relaxing this restriction is a relatively minor change (it basically just means that any code that's generic over the Failure type and uses the .get() method has to add the Failure: Error bound itself).

The wrapped Error type can vary. I am in fact getting it via a try expression, and specifically the expression is known to be capable of throwing any error (in this case it's a JSONDecoder.decode() call, which rethrows any error thrown from the model object's init(from:)).

In this particular case I could use (sourceText: String, Result) but I consider that to be a sub-par design, as the callback I'm calling has no reason to ever want access to the source text in the success case. I'm specifically only providing it in the error case so my caller can attach it as additional data to error logging.

I'm the one writing this method, but crucially, I'm not the one consuming it. I need to design an API that encourages the more junior programmers on my team to do the right thing. That's my worry with writing a wrapper error type; junior programmers aren't going to dig into the type and realize it's a wrapper, they're just going to log it as-is to the error log, and this will produce undesired logging.

And more generally, I philosophically disagree with the Failure: Error bound. As I've already stated, Result should represent the success or failure of an operation, but the concept of success or failure is not intrinsically tied to Error. Using an Error type as the Failure bound makes sense in most cases, and makes it easy to interoperate with Swift's existing error handling mechanisms, but we don't need to have the bound be put on the Result type itself in order to support that. Having that bound restricts otherwise-valid use-cases, without adding any functionality. I do not see this as analogous to why throws is bound on Error as there are practical reasons for doing that, and as Result should actually be the go-to type to use for when you want something like throws where the failure type is not bound on Error.

Yes, but Binary Stability is. If you write an app against a system library that expects Result.failure to be some definition of Error - you're going to have a bad time.

I think it's great that your voice is being heard to share your opinions and driving discussions - but as many recent threads have been re-iterating:

Swift is fundamentally an opinionated language that has a Core Team making decisions and driving the direction of the language. We as a community are here to provide our perspectives to help shape the direction - but at the end of the day decisions will be made by the Core Team.

Their decisions are not entirely arbitrary, as there are many facets to pieces of the language that need to work together to achieve long-term goals of Swift: Safe, Fast, and Expressive.

As you stated:

From the Core Team's perspective, having that ease of interoperability and window for future syntactic sugar, was a requirement for continuing Swift's current definition of Expressiveness through a direct and opinionated meaning behind what a Result type is. Seeing a Result value being returned, or not conveys a specific meaning, and choosing to use it or not is a highly expressive decision made by users of the language.

As others have stated - the Standard library cannot ever cover 100% of use cases, so must instead focus on the 80/20% or even higher of use cases. Some argue that if you cannot do 100%, then it shouldn't exist at all - while many see the value in 80/20, but those arguments are seen and evaluated in every proposal.