Since Optional is an enum, we can implement pattern matching with optionals using the enumeration case pattern, which matches a case of an enum type. Swift also provides the optional pattern, which is syntactic sugar for the enumeration case pattern, and makes pattern matching with optionals more intuitive. This article starts by using the enumeration case pattern with an optional, then explores the optional pattern in detail showing how it can be used with conditional statements and loops, how extra conditions can be added, and how we can create more complex pattern matching code involving optionals.
Related article:
Contents
Using the enumeration case pattern with optionals
Swift implements the Optional
type as an enum with two cases, as shown below.
public enum Optional<Wrapped> {
case none
case some(Wrapped)
}
The two cases of the enum represent the absence and presence of a wrapped value respectively. When there is no wrapped value, the value of the optional is .none
. When the optional has a wrapped value, its value is .some
, with the wrapped value as the associated value. Since Optional
is a generic type, the Wrapped
type parameter is used to determine the type of the wrapped value at the time an Optional
instance is created.
We can use enumeration case patterns to match the cases of an optional. Although the most common use of enumeration case patterns is in switch
statement case labels, they can also be used in the case conditions of if
, while
, guard
, and for-in
statements.
Consider the following optional Int
to which we assign the value 5
. We then use enumeration case patterns with a switch
statement to check whether the optional contains a wrapped value, to unwrap and print the value if it exists, and to print a message if it does not.
var possibleInt: Int? = 5
switch possibleInt {
case let .some(int):
print(int)
case .none:
print("No int found")
}
// 5
We get the value 5
printed since that is the value wrapped by the optional. Next, we set the optional to nil
and see the expected message printed.
possibleInt = nil
switch possibleInt {
case let .some(int):
print(int)
case .none:
print("No int found")
}
// No int found
The optional pattern
Swift uses syntactic sugar to make optionals easy to initialize and use. This extends to using the enumeration case pattern with optionals. To make the syntax easier to use, Swift gives us the optional pattern, which matches values wrapped in a some(Wrapped)
case of an Optional
enum. It lets us replace .some(x)
with x?
. We can also use nil
for the .none
case of the optional.
We can rewrite the switch
statement we saw in the previous section using the optional pattern. We do it first with an optional Int
to which we have assigned the value 5
.
var possibleInt: Int? = 5
switch possibleInt {
case let int?:
print(int)
case nil:
print("No int found")
}
// 5
Next, we do the same thing with the optional set to nil
.
possibleInt = nil
switch possibleInt {
case let int?:
print(int)
case nil:
print("No int found")
}
// No int found
Functionally, the optional pattern is identical to the equivalent enumeration case pattern, so it produces exactly the same result. Like most things to do with optionals in Swift, the syntactic sugar makes the code easier to read and understand.
Optional pattern with if and guard statements
Just like the enumeration case pattern, the optional pattern can be used in the case
conditions of if
and guard
statements. The switch
statement works naturally with enums that have a number of cases. Since an optional has only two possible cases, and we are usually only looking for one of the two, use of the optional pattern with if
and guard
statements can lead to more concise code.
To demonstrate this, we assign a value to an optional Int
, and use an if
statement with the optional pattern to unwrap and print the value.
var possibleInt: Int? = 5
if case let int? = possibleInt {
print(int)
}
// 5
Next, we set the optional to nil
and check for it using an if
statement.
possibleInt = nil
if case nil = possibleInt {
print("No int found")
}
// No int found
The optional pattern can be used with the guard
statement in much the same way. Consider the following function.
func printPossibleInt(_ possibleInt: Int?) {
guard case let int? = possibleInt else {
print("No int found")
return
}
print(int)
}
We can now call this function with possibleInt
as the argument. In the first call, we assign an Int
value to the optional while in the second call we set it to nil
.
possibleInt = 5
printPossibleInt(possibleInt) // 5
possibleInt = nil
printPossibleInt(possibleInt) // No int found
Optional pattern with for-in and while loops
A common use of the optional pattern is with for-in
and while
loops to enable selective execution of the body of the loop when iterating over a collection of optionals. Here is a simple example of using an optional pattern with a for-in
loop.
Consider the following array of optionals with four elements, three of which wrap Int
values.
var possibleInts: [Int?] = [4, 3, nil, 1]
If we want to print all the wrapped Int
values using a regular for-in
loop, for each iteration we have to use an if
statement with optional binding to conditionally unwrap the optional.
for possibleInt in possibleInts {
if let int = possibleInt {
print(int)
}
}
// 4
// 3
// 1
Using an optional pattern with a for-in
loop automatically does the conditional unwrapping and binding, and the body of the loop executes only for cases where the optional binding succeeds.
for case let int? in possibleInts {
print(int)
}
// 4
// 3
// 1
If we want the body of the loop to execute only for cases where the optional is nil
, we can rewrite the above code as follows.
for case nil in possibleInts {
print("No int found")
}
// No int found
In this case, the body of the loop executes only once since the array contains one nil
.
We can also use the optional pattern with a while
loop, as shown below, which continues printing the wrapped Int
values until it encounters the first nil
, at which point the loop terminates.
var index = 0
while case let int? = possibleInts[index] {
print(int)
index += 1
if index == possibleInts.count {
break
}
}
// 4
// 3
As expected, we just get the first two wrapped values printed, since the third element is nil
.
Adding extra conditions
We can add extra conditions in all examples of the optional pattern that we have seen in earlier sections.
For a switch
statement, extra conditions can be added using a where
clause. The switch
statement below prints the Int
value wrapped by an optional only if the value is greater than 10.
var possibleInt: Int? = 11
switch possibleInt {
case let int? where int > 10:
print(int)
case _?:
print("Int found is not greater than 10")
case nil:
print("No int found")
}
// 11
Note that to make the switch
statement exhaustive, we have to add an additional case which matches any wrapped value. In such a case, we just print a message that the value found did not meet the desired condition. We see this in action in the code below when we assign the value 10
to the optional.
possibleInt = 10
switch possibleInt {
case let int? where int > 10:
print(int)
case _?:
print("Int found is not greater than 10")
case nil:
print("No int found")
}
// Int found is not greater than 10
With an if
or guard
statement that uses an optional pattern, we can add extra conditions by separating them with commas. Here is an if
statement with an extra condition.
possibleInt = 11
if case let int? = possibleInt, int > 10 {
print(int)
}
// 11
We test this by assigning the value 10
to the optional. As expected, the code below does not print anything.
possibleInt = 10
if case let int? = possibleInt, int > 10 {
print(int)
}
The following function provides an example of adding an extra condition to a guard
statement which uses an optional pattern.
func printPossibleIntGreaterThan10(_ possibleInt: Int?) {
guard case let int? = possibleInt, int > 10 else { return }
print(int)
}
In the code below, we call this function twice, once with an optional with a wrapped value greater than 10
, which prints the wrapped value, and then with 10
as the wrapped value, which does not print anything.
possibleInt = 11
printPossibleIntGreaterThan10(possibleInt) // 11
possibleInt = 10
printPossibleIntGreaterThan10(possibleInt)
Extra conditions can be added to a for-in
loop by using a where
clause, as shown below.
var possibleInts: [Int?] = [4, 3, nil, 1]
for case let int? in possibleInts where int > 1 {
print(int)
}
// 4
// 3
Here we get just two iterations of the loop since only two elements of the array meet both the conditions.
With a while
loop, just as with if
and guard
statements, we can add extra conditions by simply separating them with commas.
var index = 0
while case let int? = possibleInts[index], int > 1 {
print(int)
index += 1
if index == possibleInts.count {
break
}
}
// 4
// 3
More complex patterns
With pattern matching, we can make our patterns as complex as we need them to be to solve the problem at hand.
Consider the below protocol which represents a pet, with two properties, one representing the name of the pet and the other representing its vaccination status. Note that the name
property is an optional since a pet may or may not have a name.
protocol Pet {
var name: String? { get set }
var vaccinated: Bool { get set }
}
Next, we define a Dog
class which conforms to the Pet
protocol. The vaccinated
property is set to false
and the initializer sets the name
property to nil
if a name is not provided when calling the initializer.
class Dog: Pet {
var name: String?
var vaccinated = false
init(named name: String? = nil) {
self.name = name
}
}
Then, we define a Cat
class which also conforms to Pet
and has an implementation identical to that of Dog
.
class Cat: Pet {
var name: String?
var vaccinated = false
init(named name: String? = nil) {
self.name = name
}
}
Finally, we define a class which represents a person.
class Person {
var name: String
var pet: Pet?
init(named name: String) {
self.name = name
}
}
Note that unlike Pet
, which has an optional name
property, the name
property of Person
is not an optional, which means a person must have a name. The pet
property of Person
, however, is an optional since a person may or may not have a pet.
Having defined the required types, let’s create an array of persons.
let names = ["Teresa", "John", "Lisa", "Henry", "Roberta"]
let persons = names.map(Person.init)
Next, we assign the persons some pets.
persons[0].pet = Dog(named: "Jasper")
persons[2].pet = Cat(named: "Cuddles")
persons[3].pet = Dog()
persons[4].pet = Dog(named: "Curly")
Here is how the pets have been assigned:
- Teresa and Roberta have dogs named Jasper and Curly respectively
- Lisa has a cat named Cuddles
- Henry has an unnamed dog
- John does not have any pet
We can use the following code with pattern matching to find out which of the persons have named dogs, and the names of their dogs.
for case let (personName, dogName?) in persons.map({ ($0.name, ($0.pet as? Dog)?.name) }) {
print("\(personName) has a dog named \(dogName)")
}
// Teresa has a dog named Jasper
// Roberta has a dog named Curly
The following code does the same for the persons with named cats, and the names of their cats.
for case let (personName, catName?) in persons.map({ ($0.name, ($0.pet as? Cat)?.name) }) {
print("\(personName) has a cat named \(catName)")
}
// Lisa has a cat named Cuddles
Just as we had done in the previous section, we can use a where
clause to add extra conditions to our for-in
loop. To demonstrate this, we set the value of the boolean vaccinated
property of one of the pets to true
.
persons[4].pet?.vaccinated = true
Now we can see whose pet has been vaccinated.
for case let (personName, pet?) in persons.map({ ($0.name, $0.pet) }) where pet.vaccinated == true {
print("\(personName) has a vaccinated pet")
}
// Roberta has a vaccinated pet
We can also use a where
clause to find out which person has an unnamed dog.
for case let (personName, dog?) in persons.map({ ($0.name, $0.pet as? Dog) }) where dog.name == nil {
print("\(personName) has an unnamed dog")
}
// Henry has an unnamed dog
Finally, we find out the name of the person who does not have a pet.
for case let (personName, nil) in persons.map({ ($0.name, $0.pet) }) {
print("\(personName) does not have a pet")
}
// John does not have a pet
Note that the above result can also be achieved with a for-in
loop without an optional pattern but with a where
clause, illustrating the fact that in programming there is usually more than one way to get the same result.
for person in persons where person.pet == nil {
print("\(person.name) does not have a pet")
}
// John does not have a pet
Conclusion
Pattern matching is a powerful feature of Swift. The optional pattern provides a convenient way to implement pattern matching with optionals, using a variety of conditional statements and loops. Where required, extra conditions can be added to make the code more powerful and flexible.
Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on X.
Leave a Reply