I have many nits to pick with the details of this proposal, but Iâll start by saying I wholeheartedly support adding Result
to the standard library.
One of the standard libraryâs goals is to provide âcommon currencyâ typesâbuilding block types which are needed frequently in many different domains and systems and often need to be passed across module boundaries. Result
is one of those types. It first started to appear before we had native error handling, but it survived throws
and it will probably survive async
too. We should provide a standard, interoperable implementation for our users.
Now, on to the nits.
The names success
, failure
, Value
, and Error
I think success
and failure
are great names because they ground Result
's cases in specific semantics. The stronger our idea is of what a type is for, the better able we are to provide meaningful operations on it. Naming the cases success
and failure
helps keep our Result
type from devolving into an Either
typeâand that's a good thing, because Either
's cases are meaningless and it makes the type harder to use.
I think Value
and Error
are the right concepts for the generic parameter namesâthey should not be something like Success
and Failure
to match the casesâbut
the fact that Error
shadows a closely related standard library type is troubling. We should rename that generic parameter to ErrorType
, and probably change its counterpart to ValueType
to match.
Overall, good work in this area.
Non-Swift.Error
failures
To further ground Result
in a specific semantic, I think we should guarantee that the failure
case's associated value is a Swift.Error
. Ideally, we would take Slava's suggestion and make Swift.Error
existentials conform to Swift.Error
, but if we can't do that, I think we should drop the second generic parameter entirely and make the associated value an Error
existential.
The proposed design bends over backwards to try to support non-Swift.Error
errors, locking many useful APIs behind conditional conformances. But the case for allowing non-Error
errors is pretty thin. The example Result<(URLResponse, Data), (Error, URLResponse?)>
type is a monstrosity that would be difficult for users to handle, would disable a lot of Result
's API surface, and could be more gracefully expressed by a custom error type like:
struct URLSessionError: Error {
var underlying: Error
var response: URLResponse?
}
If we can promise that Error
is always a Swift.Error
, I think we can significantly simplify and improve the type, while also further distancing it from Either
.
Typed errors
This proposal only touches on the elephant in the room: Do we want to support typed throws
?
I think Result
should match throws
on this question; we want people to be able to move easily between the Result
world and the throws
world, and if one of them supports more type information than the other, that's going to cause a lot of stumbling.
I think we should decide now between two alternatives:
-
Result<Value>
and no typed throws
-
Result<Value, Error>
and eventual support for throws<Error>
.
I don't think we need to rush throws<Error>
into the language right away, but I think we should either commit to adding it by Swift 6, or we should drop the Error
generic parameter from Result
. We shouldn't leave this part of our error design hanging.
I don't have a particularly strong opinion about whether we should or shouldn't support typed errors; my only position is that we should make a firm decision and design our language accordingly.
The unwrapped()
method
I think we want a method like this, but I don't like the name because it's the only place where we talk about a Result
"wrapping" anything. I would call it something like value()
or get()
or check()
.
Note that, if .failure
always contained a Swift.Error
, this method would always be available. That's another reason to constrain or remove the Error
generic parameter.
The value
and error
properties
r.value
is exactly equivalent to try? r.unwrapped()
, so I don't think we need it. r.error
doesn't have an exact equivalent, but I'm not sure that it carries its weight. If we want it, maybe we should consider supporting a catch?
feature instead; then you could write catch? r.unwrapped()
.
The isSuccess
property
We do need a good way to convert a Result
to a boolean for simple branching, and I think this is a good answer.
The various map
functions
This functionality is central to graceful Result
handling. That's why I want it to look very different from what we're considering here.
To start, I think map(_:)
and mapError(_:)
should take throwing closures. This would basically mean that they dip briefly into the try
world and then immediately go back to using Result
s. For map(_:)
, there would be no difference if you didn't use throwing expressions; for mapError(_:)
, you would be able to "fix" a failure by returning a new value, or throw
a replacement error. Basically, you get a lot of extra flexibility for almost nothing. They might look something like this:
public func map<NewValue>(
_ transform: (Value) throws<Error> -> NewValue
) -> Result<NewValue, Error>
public func mapError<NewError>(
_ transform: (Error) throws<NewError> -> Value
) -> Result<Value, NewError>
(Making this work properly with a generic Error
parameter would require typed throws
. I'm not sure if that means we would need to defer adding these methods until we have typed throws
. Maybe we could use a runtime check for now?)
If we do that, we might then consider dropping the flatMap
variants. After all, flatMap { expr }
would be exactly equivalent to map { try expr.unwrapped() }
.
Finally, we might consider renaming these away from map
and mapError
to something a little less functional-programming-y. For instance, we could call them continuing
(for processing successes) and correcting
(for processing failures). We've already changed some flatMap(_:)
variants to compactMap(_:)
; maybe we should take a cue from that.
The Result.init(_: () throws -> Value)
initializer
This is a very useful convenience. I considered suggesting it should be an autoclosure, but now that I've written the section on map
functions, I actually think using a closure here too creates a nice symmetry.
The fold(onSuccess:onFailure:)
method
This is...kind of weird? It's like a general form of all of the other operations, but I'm not sure when you'd use it in practice other than for implementing those. Keep it internal
.
Missing conveniences
The biggest thing I wish this Result
type included is conveniences for converting from the (Value?, Error?)
tuples/parameters we so frequently see today. I can imagine two different ways we might do that:
public init(value: Value?, error: Error?) {
switch (value, error) {
case (_, let error?):
self = .failure(error)
case (let value?, nil):
self = .success(value)
case (nil, nil):
self = .failure(Foundation._nilObjCError)
}
}
public static func converting<Return>(
_ body: @escaping (Result) -> Return
) -> (Value?, Error?) -> Return {
return { value, error in body(Result(value: value, error: error)) }
}
We might or might not have both, and this might make more sense as part of Foundation.
There's a lot to think about (and argue about) with this proposal, but the bottom line is, I wish we'd had Result
in the standard library years ago. I just want to make sure the language and standard library speak with one voice on error typing.