Last week we covered some basics on Optionals. This week we’ll dive in a bit deeper. We’ll take a look at the implementation of the optionals, optional chaining, nil-coalescing operator and a few more goodies. Granted, you’ll probably do just fine knowing the basics, but it’s fun to know how things work. And by the end of this article you’ll see that there’s nothing magical about optionals.
‘Optional’ enum
Optionals are nothing more than enums with two cases .none and .some. Of course, the .some case has an associated value. Optionals are so widely used in swift that there’s a lot of syntactic sugar around them. For example, if you wanted to define on optional Int, you can do it like this:
var longOptional: Optional<Int> var shortOptional: Int?
Here we can see that the ‘?’ operator is actually a shorthand version of ‘Optional<Wrapped>’. If we dive a bit deeper and go to the definition of the ‘Optional<Wrapped>’ we’ll see that it’s just an enum. It has two main cases, like so:
public enum Optional<Wrapped> : ExpressibleByNilLiteral { case none case some(Wrapped) }
Of course this means that we can use the enum cases everywhere in the code. For example, if we wanted to define a function that returns an optional Int, we could write it like this:
func returnLongOptional(_ input: Int) -> Optional<Int> { if input % 2 == 0 { return .some(input) } else { return .none } }
There’s a shorter way of writing the same function, like so:
func returnShortOptional(_ input: Int) -> Int? { if input % 2 == 0 { return input } else { return nil } }
Your .none case gets replaced by nil, and the associated value gets ‘extracted’ automatically. Because of all this syntactic sugar, you probably won’t use the ‘Optional’ enum directly in your day-to-day development.
Optionals With ‘switch’ Statement
Since optionals are actually enums, it’s only natural that we can use a switch statement with optionals. You could, for example, do some custom filtering like this:
func optionalsWithSwitch(_ input: Int?) -> String { switch input { case 0?: return "Zero" case (1..<15)?: return "A small number" case let number?: return "Your number: \(number)" case nil: return "I got nothing" } }
Pretty cool, right. Let’s move on to optional chaining.
Optional Chaining
Let’s say you have a class that has properties of an optional type. For example, your object graph might look something like this:
class Engine { var name: String? } class Car { var engine: Engine? } class CarFactory { var productionLine: Car? }
If you need to access the ‘name’ property of the engine using optional binding, things might get a bit verbose:
let engine = Engine() engine.name = "Lucy" let car = Car() car.engine = engine let factory = CarFactory() factory.productionLine = car if let car = factory.productionLine, let engine = car.engine, let name = engine.name { print("engine name: ", name) } else { print("didn't get the engine name") } // prints: engine name: Lucy
While this is not as bad as the earlier versions of swift, it can still be a bit hard to read. Luckily there’s a better way of doing this using optional chaining:
if let name = factory.productionLine?.engine?.name { print("chained name: ", name) } // prints: chained name: Lucy
We’re unwrapping optional values here using the ‘?’ operator. If at any point the unwrapping operation returns nil the optional binding will fail and the ‘if let’ block won’t get executed. This is definitely a lot more readable.
Nil-coalescing operator
This is just a fancy way of saying ‘default value’ when working with optionals 🙂 Let’s say you’re trying to unwrap an optional and if it’s nil, you want to use a default value. You have a few options. The most obvious, and the most verbose would be something like this:
var engineName = "Default" if let name = factory.productionLine?.engine?.name { engineName = name } print(engineName) // prints: Lucy
You might say, well, we have a ternary operator. We could use it, but we wouldn’t improve on the readability:
var engineName = factory.productionLine?.engine?.name != nil ? factory.productionLine!.engine!.name! : "Default" print(engineName) // prints: Lucy
Nil-coalescing operator, the ‘??’ operator, gives us a much shorter and readable code:
var engineName = factory.productionLine?.engine?.name ?? "Default" print(engineName)
If you dive in the swift optional type, you’ll find the function definition for the ‘??’ infix operator:
public func ??<T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
The actual implementation is:
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T { switch optional { case .some(let value): return value case .none: return try defaultValue() } }
You might have noticed that the default value is actually a closure. This is an optimisation to make sure that the default parameter doesn’t get evaluated unless it’s actually used. The parameter is marked with @autoclosure which automatically wraps an argument to the function in a closure, thus removing the need for us to use the curly braces when we call the function.
map and flatMap
Value transformations are so common that we have a few functions built-in to the optional type just for this purpose. You’re probably used to seeing map and flatMap functions when working with collections, but you can also find them when working with optionals. Let’s say you wanted to use your optional to create another value of a different (or same) type, something like this:
let possibleNumber: Int? = Int("42") var slightlyBiggerNumber: Double? if let number = possibleNumber { slightlyBiggerNumber = Double(number) * 1.5 } print(slightlyBiggerNumber) // prints: Optional(63.0)
Here we’re parsing a string into an Int and then multiplying that Int by 1.5 if it’s not nil. We can use the map function to write a shorter version of this:
let possibleNumber: Int? = Int("42") let slightlyBiggerNumber = possibleNumber.map { Double($0) * 1.5 } print(slightlyBiggerNumber) // prints: Optional(63.0)
As we can see, this is much shorter and more readable.
flatMap is doing pretty much the same thing as map. The only difference being that flatMap will flatten the result. Meaning, if your result is an optional optional it will be flattened to an optional. Let’s examine this on an example:
let possibleNumber: Int? = Int("42") let mappedNumber = possibleNumber.map { Int("\($0 + $0)") } print(mappedNumber) // prints: Optional(Optional(84))
Because map was returning an optional, and we parsed a string into an Int, which also returns an optional, we get a double optional. We can easily get rid of this with a flatMap:
let possibleNumber: Int? = Int("42") let mappedNumber = possibleNumber.flatMap { Int("\($0 + $0)") } print(mappedNumber) // prints: Optional(84)
Now our result is nice and flattened. And this brings us to the end of this post.
Conclusion
Optionals are a crucial part of swift, and you won’t be able to escape them if you want to use the language. Understanding how they work will only make you a better developer. Last week we covered some basics on optionals that will cover most of your day-to-day development needs. But sometimes you just need a little more, I hope you learned a little bit more reading this article.
Have a nice day 🙂
Dejan.
Pingback: Throwable Optionals | agostini.tech