Update, 07 Feb 2017
As of Swift 3, some of the code below may not work properly anymore. To see what changed, read Pyramid of Doom Updated (Swift 3).
Original Post
Today we continue the topic of avoiding Swift’s Pyramid of Doom that we started in previous post, on guard
statement.
This time, we cover a feature from Swift 1.2 – it’s not a new thing, but still very handy and useful to have in your arsenal.
Every now and then I see older code that does something like this:
func pyramidOfDoom(x: Int?, y: Int?, z: Int?) {
if let x = x {
if let y = y {
if let z = z {
//do something with x, y and z
print("\(x, y, z)")
}
}
}
}
It works and was necessary in Swift 1.0 (unless you added your own workaround for it).
In Swift 1.2, if let
allows unwrapping multiple optionals, which lets us rewrite previous example into
func noPyramid(x: Int?, y: Int?, z: Int?) {
if let x = x, y = y, z = z {
//do something with x, y and z
print("\(x, y, z)")
}
}
So much better!
What’s also useful and helpful for keeping your code clean, is the ability to add where
clause to your optional binding.
It lets you check not only if optionals hold any value, but also if that value meets certain condition.
Let’s take a look at code we would write normally, using if
statement
func ifStatement(x: Int?, y: Int?, z: Int?) {
if let x = x, y = y, z = z {
if (z < 4) {
//do something with x, y and z
print("\(x, y, z)")
}
}
}
Assuming we need z
to be less than 4
- code seems pretty normal. Let’s rewrite it to use where
clause
func whereClause(x: Int?, y: Int?, z: Int?) {
if let x = x, y = y, z = z where z < 4 {
//do something with x, y and z
print("\(x, y, z)")
}
}
One less level of code indentation and a bit cleaner code. You can add one where clause per each let
statement
func multipleWhereClause(x: Int?, y: Int?, z: Int?) {
if let x = x where x < 2,
let y = y where y < 3,
let z = z where z < 4 {
//do something with x, y and z
print("\(x, y, z)")
}
}
which acts exactly the same as
func multipleWhereClause2(x: Int?, y: Int?, z: Int?) {
if let x = x, y = y, z = z where x < 2 && y < 3 && z < 4 {
//do something with x, y and z
print("\(x, y, z)")
}
}
Which version to use? It’s mostly a matter of preference and depends what you think is easier to read.
I prefer version #1, as it’s a bit more explicit and for the same reason I advise you do the same – if you can add more readability by spending few more seconds on typing additional let
statements – do so, people reading your code later will be grateful.
One important note to remember, is that assignment and unwrapping happens before the condition in where
clause is checked.
This means, if your condition is unrelated to unwrapped variables – it might be better to check the condition first. Why?
func expensiveFunction(startWith: Int) -> Int? {
//despite what you see here
//it is really expensive to call this function!
return startWith * 2
}
func isTodayThursday() -> Bool {
//I'm writing it on Tuesday, so we can optimize ;)
return false
}
func expensiveUnwrapping() {
if let x = expensiveFunction(2), y = expensiveFunction(5) where isTodayThursday() {
print("\(x, y)")
}
}
It matters how do we get our optional values. If, as in the example above, they come from a function that is really expensive to call – it would be smart to call it only if we can be sure that returned value will actually be used.
In the example above, our expensive function is being called twice, even though we don’t even get to use x
and y
variables.
For similar cases, you might use the fact that we are allowed to use one logic statement (outside of where
clause) as long as it’s the first clause in the if let
statement
func unexpensiveUnwrapping() {
if isTodayThursday(), let x = expensiveFunction(2), y = expensiveFunction(5) {
print("\(x, y)")
}
}
This way our expensive functions will be called only if our initial logic clause is true
(according to answer by Chris Lattner on google groups).
Adding one logic clause at the beginning, works exactly the same with guard
statement
func guardUnexpensive() {
guard isTodayThursday(), let x = expensiveFunction(2), y = expensiveFunction(5) else {
return
}
print("\(x, y)")
}
That’s it for now. In one of the next articles I will go into pattern matching and I’ll explain why following statements produce exactly same results. Stay tuned!
if let x = x {
printSomething()
}
if case let x? = x {
printSomething()
}
if case .Some(let x) = x {
printSomething()
}