Translator: rsenjoyer; translator: rsenjoyer; Proofreading: Numbbbbb, Yousanflics; Finalized: Forelax

Optional values are fundamental to the Swift language. I think everyone agrees that it’s been a huge boon, because it forces developers to deal with edge cases properly. The language features of optional values enable developers to find and deal with an entire class of bugs during development.

However, the API for optional values in the Swift standard library is fairly limited. If you ignore the customMirror and debugDescription attributes, the Swift document lists only a few methods/attributes:

var unsafelyUnwrapped: Wrapped { get } 
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U? 
func flatMap<U>(_ transform: (Wrapped) throws -> U?).rethrows -> U?
Copy the code

Even with so few methods, optional values are useful because Swift compensates for them syntactically with optional chains, pattern matching, if lets, or guard lets. However, in some cases, optional values tend to create multi-branching conditions. Sometimes, a very succinct approach usually allows you to express a concept in one line of code rather than in a multi-line combination of if let statements.

I sifted through the Swift project on Github and alternative implementations in other languages such as Rust, Scala, or C# to find useful additions for Optional. I’ll explain each of the 14 optional extensions below, along with a few examples for each category. Finally, I’ll write a more complex example that uses multiple optional extensions simultaneously.

Emptiness (Emptiness)

extension Optional {
    /// Return true if the optional value is null
    var isNone: Bool {
        switch self {
        case .none:
            return true
        case .some:
            return false}}/// Returns true if the optional value is not null
    var isSome: Bool {
        return! isNone } }Copy the code

This is the most basic complement to optional types. I like these additions because they remove the concept of optional nullability from the code. For details, using optional. IsSome is more concise than if optional == nil.

/ / before use
guardleftButton ! =nil, rightButton ! =nil else { fatalError("Missing Interface Builder connections")}/ / after use
guard leftButton.isSome, rightButton.isSome else { fatalError("Missing Interface Builder connections")}Copy the code

And (Or)


extension Optional {
    /// Returns optional or default values
    /// - Parameter: if the optional value is empty, the default value will be used
    func or(_ default: Wrapped) -> Wrapped {
	    return self ?? `default`}/// Returns the optional value or the value returned by the 'else' expression
    Or (else: print("Arrr"))
    func or(else: @autoclosure (a) -> Wrapped) - >Wrapped {
	    return self ?? `else`()
    }

    /// Returns the optional value or the value returned by the 'else' closure
    // For example. Optional. or(else: {
    / / /... do a lot of stuff
    / / /})
    func or(else: (a) -> Wrapped) - >Wrapped {
	    return self ?? `else`()
    }

    // return optional value if optional value is not null
    /// If null, an exception is thrown
    func or(throw exception: Error) throws -> Wrapped {
        guard let unwrapped = self else { throw exception }
        return unwrapped
    }
}

extension Optional where Wrapped= =Error {
    /// Execute 'else' if the optional value is not empty
    func or(_ else: (Error) -> Void) {
	guard let error = self else { return }
	`else`(error)
    }
}

Copy the code

Another abstraction of isNone/isSome is the ability to specify instructions to be executed when a variable is not true. This allows us to avoid writing an if or Guard branch and instead encapsulate the logic into a method that is easy to understand.

This concept is so useful that it can be defined in four different functions.

Default Value

The first extension method returns an optional or default value:

let optional: Int? = nil
print(optional.or(10)) / / print 10
Copy the code

Default Closure

The default closure is very similar to the default value, but it allows the default value to be returned from the closure.

let optional: Int? = nil
optional.or(else: secretValue * 32)
Copy the code

By using the @Autoclosure argument, we are actually using the default closure. Closure that uses default values that are automatically converted to return values. However, I prefer to keep the two implementations separate because it allows users to write closures with more complex logic.

let cachedUserCount: Int? = nil.return cachedUserCount.or(else: {
   let db = database()
   db.prefetch()
   guard db.failures.isEmpty else { return 0 }
   return db.amountOfUsers
})

Copy the code

Using OR is a good option when assigning an optional value that is empty.

if databaseController == nil {
  databaseController = DatabaseController(config: config)
}

Copy the code

The above code could be written more elegantly:

databaseController = databaseController.or(DatabaseController(config: config)
Copy the code

Throw an error

This is also a very useful addition, because it connects optional values in Swift to error handling. Depending on the code in the project, a method or function expresses this invalid behavior by throwing an error when returning an empty optional value (such as accessing a key that does not exist in a dictionary). Connecting the two makes the code clearer:


func buildCar(a) throws -> Car {
  let tires = try machine1.createTires()
  let windows = try machine2.createWindows()
  guard let motor = externalMachine.deliverMotor() else {
    throw MachineError.motor
  }
  let trunk = try machine3.createTrunk()
  if let car = manufacturer.buildCar(tires, windows,  motor, trunk) {
    return car
  } else {
    throw MachineError.manufacturer
  }
}

Copy the code

In this example, we build the car object by calling both internal and external code, the external code (external_machine and manufacturer) choosing to use optional values rather than error handling. This makes the code very complicated, and we can use or(throw:) to make the function more readable.


func build_car(a) throws -> Car {
  let tires = try machine1.createTires()
  let windows = try machine2.createWindows()
  let motor = try externalMachine.deliverMotor().or(throw: MachineError.motor)
  let trunk = try machine3.createTrunk()
  return try manufacturer.buildCar(tires, windows,  motor, trunk).or(throw: MachineError.manufacturer)
}

Copy the code

Handling Errors

The part of the code above that throws exceptions becomes more useful when the code includes Stijn Willems’s Github free function. Thanks to Stijn Willems for the advice.

func should(_ do: (a) throws -> Void) - >Error? {
    do {
		try `do` ()return nil
    } catch let error {
		return error
    }
}
Copy the code

This free function (optional, as an optional class method) uses the do {} catch {} block and returns an error. If and only if the DO block catches an exception. Take the following Swift code as an example:

do {
  try throwingFunction()
} catch let error {
  print(error)
}
Copy the code

This is one of the basic principles of error handling in Swift, but it’s not straightforward enough. Using the functions provided above, you can keep the code simple enough.

should { try throwingFunction) }.or(print($0))
Copy the code

I think there are many cases where error handling works better.

Transformation (Map)

As you can see above, map and flatMap are all the methods provided by the Swift standard library on the optional list. In most cases, however, they can be modified slightly to make them more versatile. There are two extension maps that allow you to define a default value, similar to the implementation of OR above:

extension Optional {
    /// The optional transformation returns, if the optional value is null, the default value is returned
    /// - fn: closure of the mapping value
    /// - Parameter default: if the optional value is empty, it is returned
    func map<T>(_ fn: (Wrapped) throws -> T.default: T) rethrows -> T {
	    return try map(fn) ?? `default`}/// Returns the optional value transformation, and if the optional value is empty, the 'else' closure is called
    /// - fn: closure of the mapping value
    Else: The function to call if The optional is empty
    func map<T>(_ fn: (Wrapped) throws -> T.else: () throws -> T) rethrows -> T {
	    return try map(fn) ?? `else` ()}}Copy the code

The first method allows you to map optional values to a new type T. If optional values are empty, you can provide a default value of type T:

let optional1: String? = "appventure"
let optional2: String? = nil

/ / before use
print(optional1.map({$0.count})??0)
print(optional2.map({$0.count})??0)

/ / after use
print(optional1.map({$0.count }, default: 0)) // prints 10
print(optional2.map({$0.count }, default: 0)) // prints 0
Copy the code

The changes are so minor that we no longer need to use? Operator, replaced by the more intentional default value.

The second method is similar to the first, except that it accepts (again) a closure of type T instead of using a default value. Here’s a simple example:

let optional: String? = nil
print(optional.map({$0.count }, else: { "default".count })
Copy the code

Combining Optionals

This category contains four functions that allow you to define relationships between multiple options.

extension Optional {
    /// When the optional value is not empty, unpack and return the parameter 'optional'
    func and<B>(_ optional: B?) -> B? {
		guard self! =nil else { return nil }
	    return optional
    }

    // unpack optional values, execute the 'then' closure if the optional values are not empty, and return the execution result
    /// allows you to concatenate multiple options together
    func and<T>(then: (Wrapped) throws -> T?).rethrows -> T? {
		guard let unwrapped = self else { return nil }
	    return try then(unwrapped)
    }

    /// combine the current optional value with other optional values
    /// The combination succeeds if and only if neither of the two optional values is null, otherwise null is returned
    func zip2<A>(with other: Optional<A>)- > (Wrapped.A)? {
		guard let first = self.let second = other else { return nil }
	    return (first, second)
    }

    /// combine the current optional value with other optional values
    /// The combination succeeds if and only if none of the three optional values is null, otherwise null is returned
    func zip3<A, B>(with other: Optional<A>, another: Optional<B>)- > (Wrapped.A.B)? {
		guard let first = self.let second = other,
	      	let third = another else { return nil }
		return (first, second, third)
    }
}

Copy the code

Each of the above four functions takes an optional value as an argument and eventually returns an optional value, however, they implement it in completely different ways.

Dependencies

And (_ optional) is very useful if the unpacking of an optional value is only a prerequisite for unpacking another optional value:

/ / before use
ifuser ! =nil.let account = userAccount() ...

/ / after use
if let account = user.and(userAccount()) ...

Copy the code

In the example above, we are not interested in the specific contents of the user, but we require that the userAccount function be non-null before calling it. Although this relationship could also use user! = nil, but I think and makes their intent clear.

Chaining

And

(then:) is another very useful function that links multiple alternatives so that the unpacked value of alternative A is taken as input to alternative B. Let’s start with a simple example:

protocol UserDatabase {
  func current(a) -> User?
  func spouse(of user: User) -> User?
  func father(of user: User) -> User?
  func childrenCount(of user: User) -> Int
}

let database: UserDatabase=...// Think about how to express the following relationship:
// Man -> Spouse -> Father -> Father -> Spouse -> children

/ / before use
let childrenCount: Int
if let user = database.current(), 
   let father1 = database.father(user),
   let father2 = database.father(father1),
   let spouse = database.spouse(father2),
   let children = database.childrenCount(father2) {
  childrenCount = children
} else {
  childrenCount = 0
}

/ / after use
let children = database.current().and(then: { database.spouse($0) })
     .and(then: { database.father($0) })
     .and(then: { database.spouse($0) })
     .and(then: { database.childrenCount($0) })
     .or(0)
Copy the code

Using the AND (then) function is a big improvement to the code. First, you don’t have to declare temporary variable names (user, father1, father2, spouse, children), and second, the code is much cleaner. Also, using OR (0) is more readable than letting childrenCount.

Finally, the original Swift code was prone to logic errors. You may not have noticed, but there is a bug in the example. When writing code like that, it’s easy to introduce copy-and-paste errors. Did you observe that?

Yes, the children property should be created by calling database.childrencount (spouse), but I wrote database.childrencount (father2) instead. Such mistakes are hard to spot. Using and(then:) is easy to spot because it uses the variable $0.

Combination (Zipping)

Another extension of the existing Swift concept, zip can combine multiple optional values that together succeed or fail to unpack. In the code fragment above, I provide the zip2 and zip3 functions, but you could also name them zip22 (ok, maybe with a slight impact on rationality and compilation speed).

// A normal example
func buildProduct(a) -> Product? {
  if let var1 = machine1.makeSomething(),
    let var2 = machine2.makeAnotherThing(),
    let var3 = machine3.createThing() {
    return finalMachine.produce(var1, var2, var3)
  } else {
    return nil}}// Use the extension
func buildProduct(a) -> Product? {
  return machine1.makeSomething()
     .zip3(machine2.makeAnotherThing(), machine3.createThing())
     .map { finalMachine.produce($0.1, $0.2, $0.3)}}Copy the code

Less code, cleaner code, more elegant. However, there is also a disadvantage, is more complex. Readers must know and understand ZIP to fully grasp it.

On

extension Optional {
    // execute the 'some' closure when the optional value is not null
    func on(some: (a) throws -> Void) rethrows {
	if self! =nil { try some() }
    }

    // Execute the closure 'none' when the optional value is null
    func on(none: (a) throws -> Void) rethrows {
	if self= =nil { try none()}}}Copy the code

The above two extensions allow you to perform some additional operations, whether the optional values are null or not. In contrast to the methods discussed above, these two methods ignore optional values. On (some:) executes the closure some if the optional value is not empty, but the closure some does not take the optional value.

// Log out if the user does not exist
self.user.on(none: { AppCoordinator.shared.logout() })

// Connect to the network when the user is not empty
self.user.on(some: { AppCoordinator.shared.unlock() })
Copy the code

Various

extension Optional {
    /// Optional values are returned only if they are not empty and satisfy the conditions of 'predicate', otherwise 'nil' is returned
    func filter(_ predicate: (Wrapped) -> Bool) - >Wrapped? {
		guard let unwrapped = self,
	    	predicate(unwrapped) else { return nil }
		return self
    }

    /// Return if the optional value is not null, otherwise crash
    func expect(_ message: String) -> Wrapped {
        guard let value = self else { fatalError(message) }
        return value
    }
}
Copy the code

Filter

This method is similar to a guardian in that the package is unpacked only if the optional values satisfy the predicate condition. For example, we want all our existing users to upgrade to premium accounts so they can stay with us longer.

// Only users with id < 1000 will be affected
//
if let aUser = user, user.id < 1000 { aUser.upgradeToPremium() }

/ / use ` filter `
user.filter({$0.id < 1000})? .upgradeToPremium()Copy the code

Here, user.filter is more natural to use. In addition, its implementation is similar to the functionality in the Swift collection.

Expect (= Expect)

This is one of my favorite features. I borrowed this from Rush language. I try to avoid forcibly unpacking anything in the code base. Similar to implicit unpack option.

However, when building a UI using a visual interface in a project, it is common to see the following:

func updateLabel(a) {
  guard let label = valueLabel else {
    fatalError("valueLabel not connected in IB")
  }
  label.text = state.title
}
Copy the code

The obvious alternative is to force unpack labels, which can cause an application to crash like a fatalError. However, I must insert! , when causing a program crash,! Does not give a clear error message. Here, using the expect function implemented above is a better option:

func updateLabel(a) {
  valueLabel.expect("valueLabel not connected in IB").text = state.title
}
Copy the code

Example:

So far we have implemented a series of very useful optional extensions. I’ll give a composite example to better understand how these extensions can be used together. First, we need to explain the example. Forgive me for using this unfortunate example:

Suppose you worked for a software company in the 1980s. Lots of people write apps and games for you every month. You need to track sales, you receive an XML file from your accountant, you need to parse it and put the results into a database (how wonderful it would have been if Swift and XML had existed in the 80s). Your software system has an XML parser and a database (both written in 6502 ASM, of course) that implements the following protocols:

protocol XMLImportNode {
    func firstChild(with tag: String) -> XMLImportNode?
    func children(with tag: String)- > [XMLImportNode]
    func attribute(with name: String) -> String?
}

typealias DatabaseUser = String
typealias DatabaseSoftware = String
protocol Database {
    func user(for id: String) throws -> DatabaseUser
    func software(for id: String) throws -> DatabaseSoftware
    func insertSoftware(user: DatabaseUser, name: String, id: String, type: String, amount: Int) throws
    func updateSoftware(software: DatabaseSoftware, amount: Int) throws
}
Copy the code

The XML file might look like this:

<users>
 <user name="" id="158">
  <software>
   <package type="game" name="Maniac Mansion" id="4332" amount="30" />
   <package type="game" name="Doom" id="1337" amount="50" />
   <package type="game" name="Warcraft 2" id="1000" amount="10" />
  </software>
 </user>
</users>
Copy the code

The code to parse the XML is as follows:

enum ParseError: Error {
    case msg(String)}func parseGamesFromXML(from root: XMLImportNode, into database: Database) throws {
    guard let users = root.firstChild(with: "users")? .children(with:"user") else {
	throw ParseError.msg("No Users")}for user in users {
	guard let software = user.firstChild(with: "software")?
		.children(with: "package"),
	    let userId = user.attribute(with: "id"),
	    let dbUser = try? database.user(for: userId)
	    else { throw ParseError.msg("Invalid User")}for package in software {
	    guard let type = package.attribute(with: "type"),
	    type == "game".let name = package.attribute(with: "name"),
	    let softwareId = package.attribute(with: "id"),
	    let amountString = package.attribute(with: "amount")
	    else { throw ParseError.msg("Invalid Package")}if let existing = try? database.software(for: softwareId) {
		try database.updateSoftware(software: existing, 
					      amount: Int(amountString) ?? 0)}else {
		try database.insertSoftware(user: dbUser, name: name, 
					      id: softwareId, 
					    type: type, 
					  amount: Int(amountString) ?? 0)}}}}Copy the code

Let’s apply what we’ve learned above:

func parseGamesFromXML(from root: XMLImportNode, into database: Database) throws {
    for user in try root.firstChild(with: "users")
		    .or(throw: ParseError.msg("No Users")).children(with: "user") {
	let dbUser = try user.attribute(with: "id")
		    .and(then: { try? database.user(for: $0) })
		    .or(throw: ParseError.msg("Invalid User"))
	for package in (user.firstChild(with: "software")?
		    .children(with: "package")).or([]) {
	    guard (package.attribute(with: "type")).filter({$0= ="game" }).isSome
		else { continue }
	    try package.attribute(with: "name")
		.zip3(with: package.attribute(with: "id"), 
		   another: package.attribute(with: "amount")).map({ (tuple) -> Void in
		    switch try? database.software(for: tuple.1) {
		    case lete? :try database.updateSoftware(software: e, 
							       amount: Int(tuple.2).or(0))
		    default: try database.insertSoftware(user: dbUser, name: tuple.0, 
							   id: tuple.1, type: "game", 
						       amount: Int(tuple.2).or(0))
		    }
		}, or: { throw ParseError.msg("Invalid Package")})}}}Copy the code

If we compare, at least two things stand out:

  1. Less code
  2. The code looks more complex

When using optional extensions in combination, I deliberately create an overload state. Some of them are appropriate, but others are not. However, the key to using extensions is not over-dependency (as I did above), but whether the extensions make the semantics clearer. Compare the two implementations above and, in the second implementation, consider whether it is better to use the functionality provided by Swift itself or to use the optional extensions.

That’s all for this article, thanks for reading!

This article is translated by SwiftGG translation team and has been authorized to be translated by the authors. Please visit swift.gg for the latest articles.