Swiftwater Logo

The Nil-Coalescing Operator (??)

This entry is part 20 of 21 in the series Swiftwater

ABSTRACT

One of the more curious (and kinda cool) operators in the Swift Programming Language is the Nil-Coalescing Operator (or “??“). This operator will conditionally assign a value, based on whether or not a certain optional value or function/statement resolves to nil.

The basic form of the operator is:

let A = B ?? C

Where A is an ordinary (non-optional) value, set by examining B, which is an optional value (or function/statement). If B is not nil (has a value), then the value of B is assigned to A. If B is nil, then the value of C is assigned to A.

B always has to be an optional (either “full” optional, or implicitly-unwrapped). It can be a computed property or a function return. C needs to be whatever is required to satisfy the type for A.

The nil-coalescing operator will always force-unwrap optionals before assigning them, so A does not need to be an optional type. In fact, if the final value (in this case, C) is a full optional, the statement won’t compile. However, it can be an implicitly-unwrapped optional; in which case, you may encounter a runtime error if it is nil, and the statement tries to assign it.

QUICK “TEACH GRANDMA TO SUCK EGGS” NOTE

Swift has mainstreamed the world of optionals in Apple development. This isn’t a new concept, but Swift “bakes” it into the language, itself. The nil-coalescing operator is one way that works.

A lot of Swift syntax was designed as “syntactic sugar,” or ways to provide a more seamless expression to the programmer than other languages may exhibit. These “sugary” elements may not be programmatically necessary, but help Swift programmers to write and understand the language in a fairly efficient manner.

When it comes down to it, there’s really no “programmatic” requirement for a language past good old Machine Language. Everything after that, has been designed to help humans to write better code. If we ever get to pure AI-written software, it’s entirely possible that Machine Language may make a comeback. After all, it really is the most efficient expression of code, if you have the capability to understand it (which an AI would have).

The nil-coalescing operator gives us a quick “sugary” way to express a rather complex “quantum state” type of thing; where a value can either be a certain type (or class), or completely gone. In a strongly-typed language (like Swift), this is kind of a paradox.

Strongly-typed languages try to have as much of the analysis and “vetting” done at compile time or link time, as possible. It allows the resulting executable to be highly efficient and safe, as all of the “dicey” stuff has been sorted and optimized by runtime.

EXAMPLES

Here’s an example of how we might apply the nil-coalescing operator.

First, let’s say that we have a variable that is an implicitly-unwrapped optional:

var couldbeNil: [Int]!

This could either contain an Array of Int, or be nil (basically undefined).

And we want to assign it to a standard Array of Int variable, defined like so:

var ifVar: [Int]

The Standard if…else Way

If we were using a regular if...else evaluation, it might look like this:

if nil == couldbeNil {
    ifVar = [1,2,3,4,5]
} else {
    ifVar = couldbeNil
}

In the above, we make an explicit test of the optional before referencing its value (Note that I have the “nil” declared before the variable. That’s an old technique for ensuring that comparisons don’t accidentally set a variable. It’s not as necessary, these days, as it used to be, but it has become habit). If it is nil, then we ignore it, and set the value using a literal. If it is not nil, then we know that it is an Array of Int (since this is a strongly-typed language, there’s no question), and we set the value from that.

Using the Ternary Operator

It’s also possible to use the ternary operator:

ifVar = nil == couldbeNil ? [1,2,3,4,5] : couldbeNil

That’s a bit “sweeter,” but we still have that somewhat awkward nil == couldbeNil test, and we already know what to do, if the optional is not nil.

Using the Nil-Coalescing Operator

Think of the nil-coalescing operator as a “specialization” of the ternary operator. The above would be expressed as such, using the nil-coalescing operator:

ifVar = couldbeNil ?? [1,2,3,4,5]

In the above, the nil == couldbeNil test is implied, and the “not nil” branch is also implied, so the only part that we need to supply is the “if nil” part ([1,2,3,4,5] literal, in this case).

Isn’t that sweeter?

Be Careful How You Use the Nil-Coalescing Operator

The ternary operator is a rather controversial element. It’s really very convenient, but can sometimes lead to inscrutable code.

The same goes for the nil-coalescing operator.

In many cases, it’s a good idea to add a quick comment, to show what the operator is doing, like so:

// If the preceding step failed to result in a value, we default to the basic literal.
ifVar = couldbeNil ?? [1,2,3,4,5]

Cascading Nil-Coalescing Operators

As in many Swift usages, it’s possible to “cascade” an operation, so if one part fails a test, another part is applied. This can be done with the nil-coalescing operator. Here’s an example:

var couldbeNil1: [Int]? = [0,1,2,3,4]
var couldbeNil2: [Int]! = [5,6,7,8,9]
var couldbeNil3: [Int]! = [10,11,12,13,14]

let ifVar: [Int] = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

In the above example, we have three optionals. As written above, all of them are defined with values, so none are nil.

When we run the assignment, the following code path is followed:

let ifVar: [Int] = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

Since the first test passes the nil test, its value is assigned. Note that couldbeNil1 is a “full” (not implicitly-unwrapped) optional, and it is unwrapped by the assignment into a non-optional value.

couldbeNil1 = nil
let ifVar: [Int] = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

In this case, couldbeNil1 is forced to nil, so the first nil-test “fails,” and execution “falls through” to the next step, where we assign couldbeNil2.

Now, let’s set the second test to nil:

couldbeNil1 = nil
couldbeNil2 = nil
let ifVar: [Int] = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

And finally, the third:

couldbeNil1 = nil
couldbeNil2 = nil
couldbeNil3 = nil
let ifVar: [Int] = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

Why You Need to be Careful

If the engineers that will maintain your code will be experienced Swift programmers, then there’s really no issue with cascading nil-coalescing operators.

If, however, they are expected to be fairly new to the Swift Programming Language, they may have some difficulty deciphering a cascaded nil-coalescing operator, so make sure that it’s necessary.

One huge advantage of using the nil-coalescing operator, is that you can assign to a “let” value (a constant). Swift is designed to optimize for non-mutable values, so it’s always preferable to use “let.”

For example, if we wanted to break up this cascaded nil-coalescing operator:

let ifVar: [Int] = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

It could probably end up looking like this:

var ifVar: [Int]! = couldbeNil1
ifVar = ifVar ?? couldbeNil2
ifVar = ifVar ?? couldbeNil3
ifVar = ifVar ?? []

That’s ugly.

That said, however, it’s fairly clear what’s happening. If this were code that I was handing off to someone less experienced than I, and the value needed to be a variable (as opposed to a non-mutable value), then I might consider using this structure.

In any case, a cascaded nil-coalescing operator is still much more readable than a cascaded ternary operator:

let ifVar: [Int] = nil != couldbeNil1 ? couldbeNil1! : nil != couldbeNil2 ? couldbeNil2 : nil != couldbeNil3 ? couldbeNil3 : []

Which you could probably “improve” by using parentheses:

let ifVar: [Int] = (nil != couldbeNil1) ? couldbeNil1! : ((nil != couldbeNil2) ? couldbeNil2 : ((nil != couldbeNil3) ? couldbeNil3 : []))

Um…yeah. Let’s just stick with the nil-coalescing operator, eh?

All that said, it’s quite possible to get rather “exotic” with cascaded nil-coalescing operators:

var fullOptional2: [Int]? = array1
var fullOptional3: [Int]? = array2

func returnThisValue(_ inValue: Int, fromThisArray inArray: [Int]) -> Int? {
    for value in inArray where inValue == value {
        return value
    }
    return nil
}

let tester1 = 25
let tester2 = 1000
let tester4 = 13

let someInt: Int = returnThisValue(tester1, fromThisArray: []) ?? returnThisValue(tester1, fromThisArray: fullOptional2 ?? []) ?? returnThisValue(tester1, fromThisArray: fullOptional3 ?? [])  ?? returnThisValue(tester2, fromThisArray: fullOptional2 ?? []) ?? returnThisValue(tester2, fromThisArray: fullOptional3 ?? []) ?? returnThisValue(tester4, fromThisArray: array3) ?? nil ?? 0

SPOILER: The value will be set to 13.

CONCLUSION

The nil-coalescing operator is one of the ways that Swift safely integrates optionals into the main language. It allows single-line assignment to non-mutable values without a need for a lot of testing syntax. Single-line assignment is important, as non-mutable values can only be set once.

The nil-coalescing operator will safely unwrap (and enforce unwrapping) of explicit optionals (optionals that are not implicitly unwrapped).

It should be noted, that, like most powerful language constructs, the nil-coalescing operator can be used to create rather inscrutable code, so it should be used judiciously.

SAMPLE PLAYGROUND

// ##########################################################
// SET UP FOUR CONSTANT ARRAYS OF INT
// ##########################################################
let array1 = [0,1,2,3,4]
let array2 = [5,6,7,8,9]
let array3 = [10,11,12,13,14]
let array4 = [15,16,17,18,19]

// ##########################################################
// WE USE AN IMPLICITLY UNRWAPPED OPTIONAL
// ##########################################################

// couldBeNil is an implicitly unwrapped unwound optional, and it is a var.
var couldbeNil: [Int]!

// The compiler lets you get away with these. Note they are declared "let" (constant, or immutable).
let couldBeNilButIsnt: [Int]!
let couldBeNilButIsntReally: [Int]?

// Because Swift allows a "first one is free" setting of declared constants
couldBeNilButIsnt = array1
couldBeNilButIsntReally = array1

// You get a compiler error if you try this.
//couldBeNilButIsnt = array2
//couldBeNilButIsntReally = array2

// ##########################################################
// START BY SETTING THE IMPLICITLY UNRWAPPED OPTIONAL TO ARRAY ONE
// ##########################################################

// We initialize the implicitly unwrapped optional to an Array of the first five integers.
couldbeNil = array1

// This variable will be used to receive the set Arrays. Note that it is not optional.
var ifVar: [Int]

// All of these will end up with [0,1,2,3,4]
// DIRECT ASSIGNMENT
// Assigning it to a regular (non-optional) Array works fine. Just like it says on the tin.
ifVar = couldbeNil

// THE IF...ELSE CLAUSE
if nil == couldbeNil {
    ifVar = array2
} else {
    ifVar = couldbeNil
}

// THE TERNARY OPERATOR
// Since the implicitly unwrapped optional is non-nil, we use its value
ifVar = nil == couldbeNil ? array2 : couldbeNil

// THE NIL-COALESCING OPERATOR
// If we use the nil-coalescing operator in the assignment, the first choice is used in the assignment, as the implicitly unwrapped optional is non-nil.
ifVar = couldbeNil ?? array2

// ##########################################################
// NOW, CLEAR THE IMPLICITLY UNRWAPPED OPTIONAL
// ##########################################################

couldbeNil = nil

// All of these will end up with [5,6,7,8,9]
// DIRECT ASSIGNMENT
// This would result in a runtime error. You can't assign nil to a regular (non-optional) Array
//ifVar = couldbeNil

// THE IF...ELSE CLAUSE
if nil == couldbeNil {
    ifVar = array2
} else {
    ifVar = couldbeNil
}

// THE TERNARY OPERATOR
// Since the implicitly unwrapped optional is nil, we use array2
ifVar = nil == couldbeNil ? array2 : couldbeNil

// THE NIL-COALESCING OPERATOR
// If we use the nil-coalescing operator in the assignment, the second choice is used in the assignment, as the implicitly unwrapped optional is nil.
ifVar = couldbeNil ?? array2

// ##########################################################
// SET THE IMPLICITLY UNRWAPPED OPTIONAL TO ARRAY THREE
// ##########################################################

couldbeNil = array3

// All of these will end up with [10,11,12,13,14]
// DIRECT ASSIGNMENT
// Assigning it to a regular (non-optional) Array works fine. Just like it says on the tin.
ifVar = couldbeNil

// THE IF...ELSE CLAUSE
if nil == couldbeNil {
    ifVar = array2
} else {
    ifVar = couldbeNil
}

// THE TERNARY OPERATOR
// Since the implicitly unwrapped optional is non-nil, we use its value
ifVar = nil == couldbeNil ? array2 : couldbeNil

// THE NIL-COALESCING OPERATOR
// If we use the nil-coalescing operator in the assignment, the first choice is used in the assignment, as the implicitly unwrapped optional is non-nil.
ifVar = couldbeNil ?? array2

// ##########################################################
// CASCADED NIL-COALESCING OPERATORS
// ##########################################################
// Now that we see how the nil-coalescing operator works in general, lets' go a little deeper down the rabbit-hole.

// We create a couple more implicitly unwrapped optionals.
var couldbeNil1: [Int]? = array1    // Note that this is a "full" (not implicitly-unwrapped) optional.
var couldbeNil2: [Int]! = array2
var couldbeNil3: [Int]! = array3

// This will be set to [0,1,2,3,4]. Note that we don't need to force-unwrap the full optional.
ifVar = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

// This is the equivalent of this:
if nil != couldbeNil1 {
    ifVar = couldbeNil1!    // Note that we need to force-unwrap the full optional, here.
} else if nil != couldbeNil2 {
    ifVar = couldbeNil2
} else if nil != couldbeNil3 {
    ifVar = couldbeNil3
} else {
    ifVar = []
}

// Or using this ternary operator:
// Again, note that we need to force-unwrap the full optional here.
ifVar = nil != couldbeNil1 ? couldbeNil1! : nil != couldbeNil2 ? couldbeNil2 : nil != couldbeNil3 ? couldbeNil3 : []

// You could try improving the redability with parentheses.
ifVar = (nil != couldbeNil1) ? couldbeNil1! : ((nil != couldbeNil2) ? couldbeNil2 : ((nil != couldbeNil3) ? couldbeNil3 : []))

// Now, we set the first part of the test to nil.
couldbeNil1 = nil

// This will be set to [5,6,7,8,9]
ifVar = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

// The next part...
couldbeNil2 = nil

// This will be set to [10,11,12,13,14]
ifVar = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

// And finally, the last part:
couldbeNil3 = nil

// This will be set to []
ifVar = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

// Set the middle test to a value:
couldbeNil2 = array2

// This will be set to [5,6,7,8,9]
ifVar = couldbeNil1 ?? couldbeNil2 ?? couldbeNil3 ?? []

// ##########################################################
// VARIOUS TYPES OF VALUES AND REFERENCES
// ##########################################################
var fullOptional: [Int]? = array1
var implictOptional: [Int]! = array2
var notOptional: [Int] = array3

// This method will return either nil, or an Array of Int, as a full optional.
// If the optional parameter is set to true (default is false), nil is returned. Otherwise, we get a value.
func thisFuncReturnsAnOptionalResult(gimmeANil: Bool = false) -> [Int]? {
    return gimmeANil ? nil : [15,16,17,18,19]
}

// This will be set to [15,16,17,18,19], as the first test "passes."
let test1: [Int] = thisFuncReturnsAnOptionalResult() ?? implictOptional

// This will be set to [5,6,7,8,9] (the implicitly-unwrapped optional will evaluate as if it were a normal, non-optional value).
let test2: [Int] = thisFuncReturnsAnOptionalResult(gimmeANil: true) ?? implictOptional

// This won't work (compile-time error), because the second argument is a full optional that is defined to be possibly nil.
//let test4: [Int] = thisFuncReturnsAnOptionalResult(gimmeANil: true) ?? fullOptional

// The "correctest" way to deal with this issue is like so:
// That gives us a "fallback," in case the second argument is nil. It will set test4 to [0,1,2,3,4]
let test4: [Int] = thisFuncReturnsAnOptionalResult(gimmeANil: true) ?? fullOptional ?? notOptional

// If you do this:
implictOptional = nil

// This will have a runtime error, when the second, nil value is assigned.
// The fact it is implicitly-unwrapped means that it passes compile-time checks.
//let test3: [Int] = thisFuncReturnsAnOptionalResult(gimmeANil: true) ?? implictOptional

// This will work, as the compiler knows that the second argument is an optional, even though it's pretending not to be one.
let test3: [Int] = thisFuncReturnsAnOptionalResult(gimmeANil: true) ?? implictOptional ?? notOptional

// ##########################################################
// YOU WANNA SEE SOMETHING REALLY SCARY?
// ##########################################################
// It's possible to abuse cascading nil-coalescing operators, like so:

var fullOptional2: [Int]? = array1
var fullOptional3: [Int]? = array2

func returnThisValue(_ inValue: Int, fromThisArray inArray: [Int]) -> Int? {
    for value in inArray where inValue == value {
        return value
    }
    return nil
}

let tester1 = 25
let tester2 = 1000
let tester4 = 13

// SPOILER: This will set someInt to 13
let someInt: Int = returnThisValue(tester1, fromThisArray: []) ?? returnThisValue(tester1, fromThisArray: fullOptional2 ?? []) ?? returnThisValue(tester1, fromThisArray: fullOptional3 ?? [])  ?? returnThisValue(tester2, fromThisArray: fullOptional2 ?? []) ?? returnThisValue(tester2, fromThisArray: fullOptional3 ?? []) ?? returnThisValue(tester4, fromThisArray: array3) ?? nil ?? 0