Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
Swift’s error-related syntax calls attention to possible errors through try
and throws
. The do
/catch
syntax clearly separates the happy path (no errors) from the sad path (errors):
func exampleSyncUsageOfThrows() -> Bool {
do {
/* happy path */
let cookie = try ezbake()
eat(cookie)
return true
} catch {
/* sad path */
return false
}
}
Because throws
is “viral”, you’re forced to address it one way or another, even if it’s by deciding to flip your lid when you hit an error by using the exploding try!
.
Swift’s error-related syntax is great when every line of code executes one after another, synchronously. But it all goes to heck when you want to pause between steps to wait for an external event, like a timer finishing or a web server getting back to you with a response, or anything else happening asynchronously.
Let’s try that example again, only scheduling the cookie-baking for later, and then waiting for the cookie to cool before scarfing it:
func exampleAsyncDoesNotPlayNiceWithThrows(completion hadDinner: @escaping (Bool) -> Void) {
ezbakeTomorrow { cookie, error in
// hope you don't forget to check for an error first!
// also hope you like optional unwrapping
guard error == nil, let cookie = cookie else {
return hadDinner(false)
}
wait(tillCool: cookie) { coolCookie, error in
guard error == nil, let coolCookie = coolCookie else {
// dog snarfed cookie?
return hadDinner(false)
}
eat(coolCookie)
hadDinner(true)
}
}
}
This approach of calling a completion closure with parameters for both the desired result and the failure explanation all marked optional is common across Cocoa APIs as well as third-party code. Correctly unpacking those arguments relies heavily on convention. That is to say, it relies heavily on you being very careful not to shoot yourself in the foot.
Because both the success value (cookie
) and the failure value (error
) might not be present, both end up being optionals. That means you end up with four cases to consider, of which two should probably never happen:
cookie
but no error
. This is unambiguous.error
but no cookie
. This is similarly unambiguous.error
AND cookie
. If you’re following classic Cocoa style, this gets lumped in with the success case, so that a successful run could, before ARC, leave error
pointing at fabulously uninitialized data or scratch errors that didn’t happen. (As you might imagine, that convention gets messed up pretty often.)error
nor cookie
. This is probably a bug in whatever’s giving you this output. But, alas, you still have to deal with it as a possibility.Result
is a popular enumeration for cleaning this up. It looks something like:
enum Result<Value> {
case success(Value)
case failure(Error)
}
This addresses all the weirdness with the conventional approach:
case
exhaustiveness ensures the error is on your radar.Just as do
/catch
lets you clearly separate handling a successful result from a failure, so does Result through switch
/case
:
func exampleAsyncLikesResult(completion hadDinner: @escaping (Bool) -> Void) {
ezbakeTomorrow { result in
switch result {
case let .success(cookie):
wait(tillCool: cookie) { result in
switch result {
// look ma, no optionals!
case let .success(coolCookie):
eat(coolCookie)
hadDinner(true)
case let .failure(_):
hadDinner(false)
}
}
case let .failure(_):
hadDinner(false)
}
}
}
Result achieves the aims of do/catch
/throw
for async code. But it can also be used for sync code. This leads to competition between Result and throws for the synchronous case:
func exampleSyncUsageofResult() {
return
ezbake()
.map({ eat($0) })
.isSuccess
}
That’s…not so pretty. It would get even uglier if there was a sequence of possibly failing steps:
// this mess…
func exampleUglierSyncResult() {
return
open("some file")
.flatMap({ write("some text", to: $0) })
.map({ print("success!"); return $0 })
.flatMap({ close($0) })
.isSuccess
}
// …translates directly to this less-mess
func exampleSyncIsLessUglyWithTry() {
do {
let file = try open("some file")
let stillAFile = try write("some text", to: file)
print("success!")
try close(stillAFile)
return true
} catch {
return false
}
}
It’s kind of easy to lose the flow in all that syntax, plus it sounds like you have a funky verbal tic with the repeated map
and flatMap
. You also have to keep deciding between (and distracting your reader with the distinction between) map
and flatMap
.
That suggests a rule of thumb: stick with throws
for synchronous code. Applying that even to mixed sync (within the body of completion callbacks) and async (did I mention completion callbacks?) code allows to play to the strengths of both throws
and Result
.
First, here’s a mechanical translation of the earlier exampleAsyncLikesResult
function:
func exampleMechanicallyBridgingBetweenAsyncAndSync(completion hadDinner: @escaping (Bool) -> Void) {
ezbakeTomorrow { result in
do {
let cookie = try result.unwrap()
wait(tillCool: cookie) { result in
do {
let coolCookie = try result.unwrap()
eat(coolCookie)
hadDinner(true)
} catch {
hadDinner(false)
}
}
} catch {
hadDinner(false)
}
}
}
Each completion accepts a Result
, but in working with it, it immediately returns to using the Swift try/throw/do/catch syntax.
try
has a try?
variant that allows to clean this up even more nicely. This is more like the code you’d likely write in the first place when using this style:
func exampleNicerBridgingBetweenAsyncAndSync(completion hadDinner: @escaping (Bool) -> Void) {
ezbakeTomorrow { result in
guard let cookie = try? result.unwrap()
else { return hadDinner(false) }
wait(tillCool: cookie) { result in
guard let coolCookie = try? result.unwrap
else { return hadDinner(false) }
eat(coolCookie)
hadDinner(true)
}
}
}
This relies on some simple helper functions to bridge between Result
and throws
.
Result.unwrap() throws
goes from Result
to throws
: The caller of an async method that delivers a Result
can then use result.unwrap()
to bridge back from Result
into something you can try
and catch
. unwrap()
is a throwing function that throws if it’s .failure
and otherwise just returns its .success
value. We saw plenty of examples earlier.
static Result.of(trying:)
goes from throws
to Result
: The implementation of async methods can use Result.of(trying:)
to wrap up the result of running a throwing closure as a Result
; this helper runs its throwing closure and stuffs any caught error in .failure
, and otherwise, wraps the result up in .success
.
This is used to implement async functions delivering a result. Since the running example delivered a Boolean, you haven’t seen this used yet. Here’s a small example:
func youComplete(me completion: @escaping (Result<MissingPiece>) -> Void) {
doSomethingAsync { boxOfPieces: Result<PieceBox> in
let result = Result.of {
let box = try boxOfPieces.unwrap()
let piece = try findMissingPiece(in: box)
return piece
}
completion(result)
}
}
What these functions are called varies across Result
implementations (I’m eagerly awaiting the Swift version of what Promises/A+ delivered for JavaScript), but whatever your Result
calls them, use them! (And if they aren’t there, you can readily write your own.)
For a concrete example of implementing these, as well as the variation in names, check out antitypical/Result’s versions:
Result.unwrap() throws
: Result.dematerialize() throwsstatic Result.of(trying:)
: Result.init(attempt:)So that’s the bottom line:
Result
as your completion callback argument.do
/catch
to work with potential errors.Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...