Cool language features: Swift guard statements
There are a lot of programming languages to choose from, and some of them boast incredibly useful features that make life as a software engineer significantly easier. While there's been a lot of convergence in modern programming languages–lambdas and destructuring are everywhere now, for instance–there are still a few gems which you can only experience in one or two different languages.
While there are some articles out there describing cool programming language features, I don't think they go deep enough. I'm going to start documenting interesting and valuable language features on this blog, starting with guard
statements in Swift.
guard
statement?
What is a All software engineers will be familiar with the two basic branch statements: if
and switch
1. Swift adds a third one to the mix called guard
. A guard
statement works similarly to an if
as it takes a condition and a subsequent block of code which executes based on the result of the condition.
If a guard
statement's condition evaluates to false
, then its block of code is executed and that block of code must either terminate execution of the program, or transfer control out of the guard
statement's enclosing scope.
Here's an example of the guard
statement in action, inside a simple divide
function:
enum MathError: Error { case divisorIsZero} func divide(dividend: Double, divisor: Double) throws -> Double { guard divisor != 0 else { throw MathError.divisorIsZero } return dividend / divisor}
Our divide
function must validate that the provided divisor
isn't zero, as division by zero is undefined. In languages other than Swift you would use a plain if
statement to perform the validation, but in Swift we can use a guard
.
Removing the throw
from inside the body of the else
clause here results in a compilation error, which provides a compile-time guarantee that invalid values don't propagate through the rest of your function. Most editors will even show you an error here at design-time while you're writing your code.
Throwing an error isn't the only way to satisfy the Swift compiler. The requirement is simply that control gets transferred out of the enclosing block, which in this case is the body of the divide
function. Returning a value would also work here–even if doing so isn't mathematically correct.
Breaking out of a loop will also keep the compiler happy, as in the following contrived case:
func getValidDivisors(from array: [Double]) -> [Double] { var divisors: [Double] = [] for value in array { guard value != 0 else { continue } divisors.append(value) } return divisors}
In the case of getValidDivisors
the value of the guard
statement becomes a lot clearer once we throw a spanner in the works, such as making our array
parameter store String
s. In order to return an array of Double
s, we'll need to parse those strings–and parsing can fail.
The guard
statement can rescue us from the "pyramid of doom" in this case. guard
statements can be used to initialize new variables–most commonly used to unwrap an optional–and unlike a match
statement in Rust or a C++ if
initializer, the binding created by a guard
is accessible from the containing scope of the guard
.
Check out the following example to see this in action:
func getValidDivisors(from array: [String]) -> [Double] { var divisors: [Double] = [] for value in array { guard let number = Double(value) else { continue } guard number != 0 else { continue } divisors.append(number) } return divisors}
Comparing this to directly translated Rust code2, we need another level of indentation:
fn get_valid_divisors(array: &Vec<String>) -> Vec<f32> { let mut divisors = Vec::new(); for value in array.iter() { if let Ok(number) = value.parse() { if number != 0f32 { continue; } divisors.push(number); } } return divisors;}
These two facets–compiler-enforced termination of control flow and the binding leaking–combine to make a very useful language feature. In Swift codebases it's not uncommon to see the guard
statement used more often than the if
statement.
guard
Discussion of While the examples above are simple, real-world codebases are often much more complicated. If the logic inside your guard
clause is a bit more complicated, it is reasonably easy for someone to come along and edit the code such that execution of the function continues after the validation has failed.
In theory tests will catch a silly mistake like this. In practice you might not have test coverage for this case and even if you do have tests, a design-time error offers a vastly improved developer experience compared to catching the error after a long test suite run.
Its ability to help protect against the pyramid of doom is useful in the context of Swift, but it's not really a killer feature for programming languages in general as there are other methods for minimizing horizontal sprawl. I could probably live without it.
Coming back to the compiler guarantees of guard
, it always feels a bit strange to me that the guard
isn't something you attach to the signature of a function. In my experience codebases end up with a lot of assertCondition
or validateInput
functions, and being able to tell the compiler that a false
return from these kind of functions is something the programmer should explicitly handle would be pretty valuable. It shouldn't be up to the call site to indicate that a false
return is exceptional behavior.
guard
Designing a better So then a "guard function" would work identically to a guard
statement, with the one difference being the location of the guard
keyword. After calling a "guard function", it is required to handle a false
return by moving control flow to somewhere else in the program lest you get a compiler error.
A strawman proposal might look something like this:
function validateDivisor(divisor: number): guard { return divisor != 0;} function doSomething(input: number): void { validateDivisor(input); // ❌ compile error // ...} function doSomethingElse(input: number): void { if (!validateDivisor()) { return; // ✅ compiles } // ...} function doAnotherThing(input: number): void { if (validateDivisor(input)) { // ... } else { // ✅ compiles throw new Error('...'); }}
Indicating a return type of guard
turns the validateDivisor
function into a guard function, in the similar manner to how specifying a return type of divisor is number
would turn our function into a type predicate. Attaching the guard
to the function itself means that the compiler is able to enforce proper usage. If validation fails, then control flow must be transferred elsewhere. Sounds simple enough, and ensures that failed validation gets handled upstream.
Actually implementing this in the general form is a nightmare. The Swift guard
statement is trivial to implement in a compiler, because there's no need to perform static analysis on the condition. Simply verifying that the body of the else
clause terminates the program or transfers control flow elsewhere is sufficient.
"Guard functions," on the other hand, would require the compiler to statically analyze the condition inside the if
statement to verify that the exceptional case is handled by the programmer. Complicated conditions involving boolean operators would be hard–if not impossible in some cases–to statically analyze. We can simplify things by requiring that an if
statement involving a guard function can't include anything else in the condition–only the guard function call, and a possible !
operator.
The only way to guarantee validation functions are used properly at the moment (in Typescript) is to directly throw
inside the validation function, but throwing is a rather disruptive thing to do. Who knows where the nearest try
/catch
is? And how are you supposed to know that this function throws without digging into its implementation?
An advantage the guard
return type has is that simply hovering over the function name and inspecting the signature is enough to know how this function impacts control flow. Even without that–the explicit return
makes things extremely obvious to the reader.
I think guard functions would be a really fantastic addition to a variety of languages. While I prefer Rust's Option
and Result
types for handling errors and validating input, the ergonomics of those types don't come from the types themselves. Rust is built around the existence of Option
and Result
, and has rich and expressive syntax for interacting with them. There are libraries for languages like Javascript which attempt to port these types over from Rust, but they always feel weird to use due to lack of pattern matching and support in the standard library.
Guard functions on the other hand–or even just plain guard
statements–fit in a lot nicer with the semantics of more typical languages. They wouldn't feel out of place at all in any of C#, Go, Java, or Javascript, because this programming language feature is a very incremental addition to the control flow structures which already exist in those languages.
Could we possibly see Swift's guard
statement proliferate to other languages? Would it be in the form of the statement, or as part of function signatures? Only time will tell.
- Even Python has
switch
statements now, if you consider its structural pattern matching to be aswitch
!↩ - This is far from idiomatic Rust. The
Vec
struct has a rich API, and we can actually write this function as a chain of functions onarray.iter()
. This is purely intended as a demonstration ofguard
's binding "leaking" out into the enclosing scope.↩