Collaborating with Paul Hudson is a pleasure but the time difference can be, well, confusing. So when I started putting together an outline about the new command line argument parser for Pragmatic, one of the first things I wrote was a command line utility to tell me what time it was in Bath, UK:
% now bath uk Bath 4:16:42 PM (GMT+1 United Kingdom Time)
I use CLGeocoder
to use whatever terms I enter after the command as the hints to look up places of interests. I grab the first match and pull the time zone from that match (or throw an error if there’s no possible match).
Command-line utilities are not particularly well known for their asynchronous feature support. Because geocodeAddressString
does run asynchronously, I use the quick and dirty trick of starting a runloop that executes until the completion handler finishes. I’m basically adapting an async method to sync.
This gave me an opportunity to finally get around to using the new Result
type. Restrictions on other projects prevented me from kicking its wheels (or I was already using my own version from way back).
I struggled a little with how to incorporate Result
. Here’s an earlier go at this. I use an optional result
to store the result, which is then set in completion scope:
var result: Result<[CLPlacemark], Error>? CLGeocoder().geocodeAddressString(hint) { placemarks, error in switch (placemarks, error) { case (_, let error?): result = .failure(error) case (let placemarks?, _): result = .success(placemarks) default: fatalError("Geocoder error, no results.") } CFRunLoopStop(CFRunLoopGetCurrent()) } CFRunLoopRun()
This code bothered me, and not just because I had to test and unwrap result
when control returned to the main part of my method. It seems so obvious that Result
should have a simple initializer based on the common elements found in legacy completion handlers. This would collapse those arguments to, for example, completion(Result(error, data))
. Unless I’m missing something big here, I thought to build my own convenience initializer:
extension Result { init(_ success: Success?, _ failure: Failure?) { precondition(!(success == nil && failure == nil)) switch (success, failure) { case (let success?, _): self = .success(success) case (_, .let failure?): self = .failure(failure) default: fatalError("Cannot initialize `Result` without success or failure") } } }
That extension allowed me to collapse the code down to this. Notice how I can use the initializer here to eliminate the optional result, simplifying my extraction with get
, towards the end. Admittedly, it’s not always easy to come up with an initial value that can be overwritten this way, but here it worked. The handler became two lines long, and processing the result to get my placemark (including all error handling), another two lines:
static func fetchPlaceMark(from hint: String) throws -> CLPlacemark { var result: Result<[CLPlacemark], Error> = Result([], nil) CLGeocoder().geocodeAddressString(hint) { placemarks, error in result = Result(placemarks, error) CFRunLoopStop(CFRunLoopGetCurrent()) } CFRunLoopRun() let placemarks = try result.get() return placemarks[0] }
I quite like my initializer and wonder why something like that doesn’t already exist unlike init(catching:
. I’m curious as surely I’ve missed something important. Even in normal completion handlers, I’d imagine using the legacy Error?
and Data?
optionals would be a common use-case for Result
…
Let me know.
4 Comments
One reason might be on display here in your examples: they behave differently when both success and error cases are non-nil. In the first case you get failure, in the second you get success. I would guess, but don’t have any examples on hand, that legacy code might not be fully consistent when it comes to disallowing this case.
I played with things a little and decided that should both be non-nil, the error will always win:
Why there are both `precondition` and `fatalError`? `fatalError` will be called only in unchecked builds and it seems weird because it would still fail but with different message. Since the fatalError can’t be skipped I would remove the `precondition`.
`fatalError` is there so it will compile. I have no expectations of that line ever being reached.