13 Sep 2018
When I first started writing Swift, I felt like Optional
s were a constant pain point. Everything seemed to end up being Optional
, resulting in me littering if let
s and guard
s around my code, and generally just making a horrible mess of things.
Pretty quickly I came to the realisation that most Swift developers come to - that the awesomeness of Optionals
is actually in types being NON-Optional. You acquire some kind of resource that may or may not exist (such as the contents of a file), unwrap it, and then from that point onwards frolic in a utopia of non-optionalness, without a null pointer exception in sight.
For the most part, I viewed Optionals
kind of like little gremlins. Your aim is to contain and confine those little gremlins to small parts of your codebase, ridding yourself of them as quickly as possible, so that they can’t wreak havoc anywhere else.
I think this is a pretty good way to approach Optional
s. Recently though, I’ve come to realise that non-optionalness isn’t always helpful, so here’s some tricks where intentionally using a bit of Optional
ity might actually be helpful:
Here’s the brief. We’re making the leaderboard for a game, and any number of players could be on the leaderboard. Crucially, the first, second and third place players will be displayed on a podium at the top of the leaderboard.
We’ve got a method that can populate the podium, taking three optional Player
s:
func showPodium(first: Player?, second: Player?, third: Player?) {}
We’ll pass the first three players from our players
array to this method. There might not be three players on the leaderboard yet though, so we have to check that each player exists before we grab it from the array:
var players = getLeaderboardPlayers()
var firstPlayer: Player?
if players.count > 0 {
firstPlayer = players[0]
}
var secondPlayer: Player?
if players.count > 1 {
secondPlayer = players[1]
}
var thirdPlayer: Player?
if players.count > 2 {
thirdPlayer = players[2]
}
showPodium(first: firstPlayer, second: secondPlayer, third: thirdPlayer)
We don’t know how many players will be in the array, so before each access we check that the array has enough items so that we don’t crash. Notice as well how we’re also technically violating the DRY principle because for each access we had to define the index twice.
Any exception can be traded for an Optional
though. Simply check for the exception condition, and return nil
if it fails.
We’ll define a subscript on Array
that returns nil
if the index is out of bounds:
extension Array {
public subscript(maybe index: Int) -> Element? {
if index > count - 1 {
return nil
} else if index < 0 {
return nil
} else {
return self[index]
}
}
}
Let’s see if that can improve our code:
var players = getLeaderboardPlayers()
showPodium(first: players[maybe: 0],
second: players[maybe: 1],
third: players[maybe: 2])
That’s so much better. Using Optional
s actually made our code easier to read and understand!
Ok, new brief. We’re making a navigational menu. It’s a list of links that open other pages of our application, that we’ll use a tableview to display.
The only issue is that what items show in the menu depends on the state of user. A user might be logged in, upgraded, and have some number of credits. So we end up with something like this:
var menuItems: [String]
if user.isLoggedIn {
menuItems.append("Manage Account")
} else {
menuItems.append("Login")
}
if !user.isUpgraded {
menuItems.append("Purchase Upgrade")
} else {
menuItems.append("Manage purchases")
}
if user.numberOfCredits < 10 {
menuItems.append("Purchase additional credits")
}
This is ok, but it’s getting a bit messy. As it grows it will become difficult to see how the menu is ordered. What we really want is a ordered list of possible elements that will appear in the menu if they are applicable to the user, and to hide all the additional logic elsewhere.
Let’s make another maybe
function, but this time for appending to an array:
extension Array {
mutating func append(maybe item: Element?) {
if let item = item {
append(item)
}
}
}
Now we can rewrite our example to make use of it:
var menuItems: [String]
menuItems.append(maybe: makeManageAccountItem())
menuItems.append(maybe: makeLoginItem())
menuItems.append(maybe: makePurchaseUpgradeItem())
menuItems.append(maybe: makeManagePurchasesItem())
menuItems.append(maybe: makeAdditionalCreditsItem())
// Each 'make' method returns an optional
func makeManageAccountItem() -> String? {
return user.isLoggedIn ? "Manage Account" : nil
}
Each make
method returns a String?
, which will be nil
if that option shouldn’t appear in the menu for the current user.
This gives us a declarative list of elements that could appear in the menu. The order is obvious from the code, and all of the conditions are handled elsewhere. Sure, we purposefully added some optionality to our code, but because we now have purpose built extensions for dealing with optionals, there’s not an if let
in sight!
In general we want things to be non-optional where possible. The Optional
type is designed to model a situation where something may or may not exist, though, and if this is what our code needs then we shouldn’t be afraid to use it.
The important thing is to build the mechanisms to work with optionals in a convenient way. Once these are in place, we can sometimes use optionality to our advantage to write more readable code.