Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
As my fellow Nerd Juan Pablo Claude pointed out in his post on Error Handling in Swift 2.0, the latest iteration of Swift introduces many features, and a new native error handling model is notable among these.
Prior to its 2.0 release, Swift did not provide a native mechanism for error handling. Errors were represented and propagated via the Foundation
class NSError
. In focusing on enumerations, the new model for error handling in Swift 2.0 feels more idiomatic.
While Juan Pablo’s post offered a bit of history and differences between Objective-C and Swift error handling, today I’d like to dive into the differences in error handling between Swift 1.2 and Swift 2.0.
In Swift 1.x, a reference to an NSError
was passed to an NSErrorPointer
parameter in a function or method to fill in error information if it exists.
And while NSError
was serviceable, its strong emphasis on string error domains and integer error codes made it difficult to examine an error and know exactly what to do with it.
If you wrote any Swift 1.x code, then you might have written something like:
var theError: NSError?
func doSomethingThatMayCauseError(error: NSErrorPointer) -> Bool {
// do stuff...
var success = true
// if there is an error, then make the error, and set the return to `false`
if error != nil {
error.memory = NSError(domain: "SuperSpecialDomain", code: -99, userInfo: [
NSLocalizedDescriptionKey: "Everything is broken."
])
success = false
}
return success
}
let success = doSomethingThatMayCauseError(&theError)
if !success {
if let error = theError {
println(error.localizedDescription) // "Everything is broken."
}
}
As the example demonstrates, error handling in Swift 1.x was constrained by an intellectual debt to NSError
. This constraint meant that code had to create an optional instance of NSError
, and pass its pointer to some function that took an NSErrorPointer
. That pointer was then later used in the function to fill it out with error information if there was a problem. If a non-nil pointer was indeed passed into the function’s error
parameter, then an appropriate error was created and assigned to the NSErrorPointer
’s memory
property.
Since the function returned true
on success, that was the first thing to check. If the return was false
, then theError
needed to be unwrapped because there was a problem. Examining the error would then give a little more information on what went wrong.
This process adds a lot of mental overhead. It adds boilerplate code (e.g., the optional binding syntax) and we even have to use a strange new type called NSErrorPointer
. It is reliant upon an Objective-C class (NSError
) and requires that we wrap a pointer to this class in a new type made just for Swift. NSError
also forces us to use optionals, but we would obviously much prefer a more straightforward mechanism: Is there an error or not? It would be better to know decisively that there is an error, but the above code instead relies upon our interrogation of an NSError
instance.
Last, NSError
does not make it easy to represent and check error information exhaustively. There are many occasions where you may get an error back, but depending upon the domain and the code, you may not actually care about that particular error. Because NSError
buries that information in its properties, you have to write code to make sure that the error you received is the one you are prepared to handle.
Swift’s new model for error handling is much more idiomatic. A new protocol called ErrorType
represents a native mechanism for representing errors. ErrorType
is designed to work specifically with enumerations. You create an enumeration to model the sorts of errors you expect to encounter. This means that you create error types that comprise a known set of some sort of error (more on this soon).
Error handling in Swift 2.0 takes advantage of Swift’s powerful enumerations. The associated values on an enum’s cases make it easy to pack additional error information into an instance of the enum. Furthermore, listing potential errors in the cases of an enum makes error handling more complete, predictable and exhaustive: You handle the errors you expect and can do something with.
Consider an example that seeks to build a naive database to house users and their movie ratings.
First, we have a Movie
type.
struct Movie {
let name: String
}
Movies are simple; they just have a name
.
Since Movie
is a value type, let’s be sure to conform to Equatable
; we will take advantage of this functionality later on.
extension Movie: Equatable {}
func ==(lhs: Movie, rhs: Movie) -> Bool {
return lhs.name == rhs.name
}
Movies can be rated with a Rating
enum.
enum Rating {
case Bad, Okay, Good
}
Movies can be .Bad
, .Okay
and .Good
.
Movies are rated by a User
. Users just have names
and emails
. Again, because User
is a value type, we will be sure to make User
conform to Equatable
.
struct User {
let name: String
let email: String
}
extension User: Equatable {}
func ==(lhs: User, rhs: User) -> Bool {
return lhs.name == rhs.name && lhs.email == rhs.email
}
User
’s conformance to Equatable
simply checks to see if the two instances have the same name and email.
A MovieRating
type ties all of these together and will represent a table for movie ratings in our database.
struct MovieRating {
let rating: Rating
let rater: User
let movie: Movie
}
Instances of MovieRating
have properties for ratings, raters and movies.
As above, we have MovieRating
conform to Equatable
.
extension MovieRating: Equatable {}
func == (lhs: MovieRating, rhs: MovieRating) -> Bool {
return lhs.rating == rhs.rating && lhs.rater == rhs.rater && lhs.movie == rhs.movie
}
We are almost ready to make our movie review database. Before we begin, it is worthwhile to consider our database’s API. Since Swift 2.0’s error handling model relies upon enumerations, it is easy to think about the sort of errors we may encounter while we are designing a type’s implmentation.
Let’s start out with this implementation of our database’s error type.
enum DatabaseError: ErrorType {
case InvalidUser(User)
case MoviePreviouslyRated(Movie)
case DuplicateEmailAddress(String)
}
The enum DatabaseError
sketches out the sort of functionality that we can expect the database to perform by listing the possible errors we may expect from it. This helps to set our expectations for the database’s API. Just glancing at this enum tells us that queries to the database may involve an invalid user, something having to do with previously rated movies, and a duplicate email address.
Notice that the database’s error type is modeled as an enumeration.
This helps the error type to comprehensively list out all of the errors that we can expect from it. The enumeration tells us that it is modeling errors via its conformance to the ErrorType
protocol. Since DatabaseError
is a regular Swift enumeration, we can even add associated values to particular cases on the enum to give useful error information to an instance of DatabaseError
.
Let’s implement our Database
to see how this enum will be used.
class Database {
private(set) var users: [User] = []
private(set) var movies: [Movie] = []
private(set) var ratings: [MovieRating] = []
func createUser(withName name: String, email: String) throws -> User {
let userEmails = users.map { $0.email }
if userEmails.contains(email) {
throw DatabaseError.DuplicateEmailAddress(email)
}
let newUser = User(name: name, email: email)
users.append(newUser)
return newUser
}
}
The Database
is a class with three stored properties.
One for the users
in the database, another for the movies
that users have rated, and a final property for movie ratings
. For simplicity, these are stored properties with default values of empty arrays.
Database
currently has only a single method—one for creating users and inserting them into the database. The method createUser
has parameters for all of the inputs needed to create a user. After the parameter list, however, there is a new keyword being used: throws
.
throws
marks the method as potentially generating an error.
For example, it is possible that somebody will try to create a user with the exact same email address as another user (which this database uses to differentiate users). If this should happen, then the function throws
an error. The error will tell the caller what went wrong.
In this case, the error will be .DuplicateEmailAddress
and will carry with it the duplicated email address in the case’s associated value.
If no error is encountered, then the method will append
the newUser
to Database
’s users
array. Last, createUser
will return the new user.
You might think that the createUser
function has the following type: (String, Int, [Movie]) -> User
. After all, that’s what the type would be in Swift 1.2. But this is Swift 2.0, and there is this weird throws
keyword. As you now know, throws
means that this method may generate an error. Indeed, throws
tells the compiler that this function is different. It is so different that it has a different type: (String, Int, [Movie]) throws -> User
.
Let’s create an instance of the Database
to make a new user and see how error handling in Swift 2.0 works.
var db = Database()
do {
var alana = try db.createUser(withName: "Alana", email: "alana@example.com")
} catch DatabaseError.DuplicateEmailAddress(let email) {
print("(email) already exists in the database.")
} catch let unknownError {
print("(unknownError) is an unknown error.")
}
Since createUser
can throw an error, we have to use a do-catch
statement to capture and respond to errors.
Inside of the do
block, we try
to call a function that may throw an error. In this case, a call to createUser
may succeed, but it also may fail. The reason that it may fail is if that new user is created with a duplicate email address.
This error is captured in the catch
block, which looks and works like a switch
statement. Here, we check to see if the error matches a specific case on the DatabaseError
enum: .DuplicateEmailAddress
. Since this case has an associated value, we can bind this value to a constant (email
in this example), and use it to respond to the error. In the example above, all we do is log that invalid email to the console. A more “real-world” application might do something more meaningful like let the user of the application know that a particular email has already been taken.
Notice that the do-catch
statement above is concluded with a “catch-all” block. This catch
does not provide any pattern to match an error against, and so it will capture any error that comes through. It does specify a new local constant to bind this uncaught error to: unknownError
. If this constant were not specified, then the compiler would have bound the error to a constant named error
to be used locally within this catch
. Similar to switch
statements that must exhaustively address each potential case, Swift’s do-catch
statements must exhaustively capture all possible errors.
The “catch-all” above works like a default
case on a switch
.
You might think that you can omit this “catch-all” by simply implementing a catch
for each possible error. Unfortunately, Swift’s compiler cannot know that you are covering all errors in this manner. All the compiler knows is that you called a function that can throw
an instance of some type that conforms to ErrorType
. Thus, it cannot know all of the possible errors that could be encountered. This limitation is overcome by using a catch
without an error pattern to match, which is called a “catch-all.”
(Incidentally, if you are following along in playground, the compiler will not require that you have this “catch-all.” However, a regular Xcode project will give you a compiler error telling you that you have not caught all possible errors if you omit the “catch-all”.)
Now that alana
is in the database, let’s give this user a movie rating.
Let’s first make an add
method on the database.
func add(movie movie: Movie, withRating rating: Rating, forUser user: User) throws {
if !users.contains(user) {
throw DatabaseError.InvalidUser(user)
}
let userRatedMovies = ratings.filter { $0.rater == user }.map { $0.movie }
if userRatedMovies.contains(movie) {
throw DatabaseError.MoviePreviouslyRated(movie)
}
if !movies.contains(movie) {
movies.append(movie)
}
ratings.append(MovieRating(rating: rating, rater: user, movie: movie))
}
The method add
takes a movie
, a rating
and a user
as arguments. It throws an error should the user
not exist in the database, or if the user
previously rated the movie
. Hence, this method can throw two potential errors.
If the user
is valid and the movie
has not been previously rated by that user, then the movie is added to the database’s ratings
property. If the movie has not already been added to the database, we also add it to the array of movies in the database.
Now we can use the add
method to give a movie rating to the database.
do {
var alana = try db.createUser(withName: "Alana", email: "alana@emaple.com")
let darko = Movie(name: "Donnie Darko")
try db.add(movie: darko, withRating: .Good, forUser: alana)
} catch DatabaseError.DuplicateEmailAddress(let email) {
print("(email) already exists in the database.")
} catch DatabaseError.InvalidUser(let user) {
print("(user) is not in the database.")
} catch DatabaseError.MoviePreviouslyRated(let movie) {
print("User has already rated (movie.name)")
} catch let unknownError {
print("(unknownError) is an unknown error.")
}
We create an instance of Movie
, then attempt to add that movie to the database. Notice that we have added two new catch
es. These help to capture errors related to alana
not being in the database and the movie Donnie Darko already being rated by alana
.
The do-catch
statement now does several things. It creates an instance of User
, creates a Movie
instance, and adds that instance to the database. The code above also catches all of the possible errors that may arise from these operations. Of course, a real app should do something more useful with these error than simply logging them to the console.
try
, but do not try!
Any method that is marked with throws
needs to be dealt with appropriately. Consider the following example:
func myFunc() throws { ... }
If you were to call myFunc
(e.g., myFunc()
), then the compiler will yell at you: Call can throw but is not marked with 'try'
. Functions (and methods) that throw
errors must be tried when called: try myFunc()
. It is up to you to handle the errors correctly, but the compiler will require that you at least try
.
In keeping with Swift’s emphasis on compile time safety, we can now mark functions as potentially throwing an error. This changes the type of the function, which differentiates a function that throws
from a function that does not. Calling a function that throws
means that you are required to try
the function when you call it.
Swift 2.0 provides us with a mechanism to short circuit this safety if needed. In the code above, myFunc
throws
some sort of error. Calling myFunc
without a try
will lead to a compiler error. However, you can circumvent this requirement by using try!
: try! myFunc()
. This will prevent the error from being forwarded on to you. It essentially tells the compiler that you do not care about the potential errors that may arise from calling the function.
As you might expect, using try!
is dangerous. The exclamation point (!
) should stir within you the same fear and responsibility it does when forcibly unwrapping optionals. If there is an error, function call marked with try!
will cause a runtime error, which will crash your application. You should only use try!
when you are absolutely sure the call will not generate an error, or if you are absolutely sure you want to crash if there is an error. In either case, try!
should be used sparingly and should require strong justification.
Swift 2.0’s model for error handling brings a native mechanism to handling errors. In brief, its use of enumerations allows for a do-catch
statement that works similarly to a switch
statement, and enumerations provide an elegant way to capture the errors that may be thrown. Functions marked with throws
must be called with try
. These functions should signal to you that you need to handle some error.
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...