Fresh-faced, amateur, and impressionable: Swift is not my main jam. When setting out to write Amatino Swift, I was hungry for best-practice and good patterns. Amatino Swift involves lots of asynchronous HTTP requests to the Amatino API.
Asynchronous programming requires bifurcation at the point of response receipt. That is, an asynchronous operation may yield a success state or a failure state. Result types are a pattern widely espoused as a pattern for handling such bifurcation.
I'd like to open a debate over whether result types should be used in Swift. After some thought, it appears to me that they are useless. I propose that we would be better off encouraging newbies to utilise existing Swift features, rather than learning about and building result type patterns.
For the purposes of this discussion, let's assume that our code includes two functions, each of which handle a bifurcated state:
func handleSuccess(data: Data) {
// Do stuff with data
}
func handleFailure(error: Error) {
// Do stuff with error
}
Inside these functions, we might implement code which is independent of our bifurcation pattern. For example, we could case-switch
on Error type in order to present a meaningful message to a user.
Now to the bifurcation itself. A naive and simple pattern might be:
// This is bad code. Do not copy it!
func asynchronousCallback(error: Error?, data: Data?) -> Void {
if (error != nil) {
handleFailure(error!)
return
}
handleSuccess(data!)
return
}
There are myriad problems with this approach. We have no type safety over data
. We do not control for cases where programmer error yields a state where both data
and error
are nil
. It's ugly. More.
Result types propose to improve upon this pattern by defining a type such as:
enum Result<Value> {
case success(Value)
case failure(Error)
}
Which may be used like so:
func asynchronousCallback(result: Result<Data>) {
switch result {
case .success(let data):
handleSuccess(data)
case .failure(let error):
handleError(error)
}
return
}
This pattern introduces type safety to both error
and data
. I suggest that it does so at too great a cost when compared to using inbuilt Swift features. Every asynchronous bifurcation now requires a switch-case
statement, and the use of a result type.
Compare the result type pattern with one that uses the Swift guard
statement:
func asynchronousCallback(error: Error?, data: Data?) {
guard let data = data else { handleError(error ?? TrainWreck()) }; return
handleSuccess(data)
return
}
In this case, we have type safety over error
and data
. We have handled a case in which a programmer failed to provide an Error
using the nil-coalescing operator ??
. We have done it all in two lines of less than 80 char. A suitable error type might be defined elsewhere as:
struct TrainWreck: Error { let description = "No error provided" }
Bifurcation via a guard
statement appears to me to have several advantages over result types:
- Brevity. Functions handling asynchronous callbacks need only implement a single line pattern before proceeding with a type safe result.
-
Lower cognitive load. A developer utilising a library written with the
guard
pattern does not need to learn how the library's result type behaves. -
Clarity. A
guard
statement appears to me to be more readable than acase-switch
. This is subjective, of course.
What do you think? I am not a Swift expert. Am I missing something obvious? Why would you choose to use a result type over a guard
statement?
Cover image - A bee harvests pollen from lavender on Thornleigh Farm
Top comments (4)
Not a swift programmer, but in general the advantage of the ADT
Result
is composability and the use of higher order functions (map, mapError, flatMap, fold), while the example is focussing only on unwrapping the datatype.I would argue that your last example, although an improvement over the initial one, still can be improved. Consider this API:
In this example, the API solves the bifurcation problem for the caller completely and provides it with a clean type-safe way of handling success or failure.
The same can be achieved by unpacking the
Result
type with observer pattern, which removes the need forswitch
statement:Or even in Rx:
To summarize, the API should provide the caller with an unambiguous representation of the result of the operation.
Result
type provides such disambiguation. Variants of the callback function with optional parameters do not.I don't develop in swift but find it interesting. That said, the result and option types in other languajes get syntactic sugar via the "do notation" or the "? operator", makes many things of what you are exposing way easier.
i also find result type strange...
BUT, maybe with higher order functions it could have a better meaning: infoq.com/news/2019/01/swift-5-res...