Home iOS & Swift Books Swift Apprentice

19
Custom Operators, Subscripts & Keypaths Written by Cosmin Pupăză

You’ve learned the basics of operator overloading in Chapter 16, “Protocols”, where you implemented the Equatable and Comparable protocols and added custom behavior to standard operators.

However, there are certain cases when overloading standard operators is simply not enough. This chapter will show you how to create custom operators from scratch and define your very own subscripts, a special case of computed properties. You’ll use subscripts to declare your own shortcuts for accessing the elements of custom types and provide keypaths as dynamic references for properties of objects.

Custom operators

You declare your own operators when you want to define custom behavior not covered by the standard operators. Think of exponentiation, for example. You could overload the multiplication operator since exponentiation means repeated multiplication. Still, it would be confusing. Operators should do only one type of operation, not two.

So you’ll define your own exponentiation operator, first only for a specific type, then extend it by making it generic. Before doing that, you need to know a little bit of theory about operator types. Time to dive in!

Types of operators

There are three major types of operators: unary, binary and ternary.

Your own operator

Let’s walk through the process of creating a new operator from scratch. We’ll create one for exponentiation. Since it’s a custom one, you get to choose the name yourself. It’s usually best to stick to the characters /, =, -, +, !, *, %, <, >, &, |, ^ and ?, although many other Unicode characters are allowed. You may need to type it often, so the fewer keystrokes, the better. Since exponentiation is repeated multiplication under the hood, it would be nice to choose something which reflects that. We’ll use ** since some other languages use this name as well.

infix operator **
func **(base: Int, power: Int) -> Int {
  precondition(power >= 2)
  var result = base
  for _ in 2...power {
    result *= base
  }
  return result
}
let base = 2
let exponent = 2
let result = base ** exponent

Compound assignment operator

Most built-in operators have a corresponding compound assignment version. Do the same for the exponentiation operator:

infix operator **=

func **=(lhs: inout Int, rhs: Int) {
  lhs = lhs ** rhs
}
var number = 2
number **= exponent

Mini-exercises

  1. Implement a custom multiplication operator for strings so that the following code works:
let baseString = "abc"
let times = 5
var multipliedString = baseString ** times
multipliedString **= times

Generic operators

You want the exponentiation operator to work for all kind of integer types. Update your operator implementations as follows:

func **<T: BinaryInteger>(base: T, power: Int) -> T {
  precondition(power >= 2)
  var result = base
  for _ in 2...power {
    result *= base
  }
  return result
}

func **=<T: BinaryInteger>(lhs: inout T, rhs: Int) {
  lhs = lhs ** rhs
}
let unsignedBase: UInt = 2
let unsignedResult = unsignedBase ** exponent

let base8: Int8 = 2
let result8 = base8 ** exponent

let unsignedBase8: UInt8 = 2
let unsignedResult8 = unsignedBase8 ** exponent

let base16: Int16 = 2
let result16 = base16 ** exponent

let unsignedBase16: UInt16 = 2
let unsignedResult16 = unsignedBase16 ** exponent

let base32: Int32 = 2
let result32 = base32 ** exponent

let unsignedBase32: UInt32 = 2
let unsignedResult32 = unsignedBase32 ** exponent

let base64: Int64 = 2
let result64 = base64 ** exponent

let unsignedBase64: UInt64 = 2
let unsignedResult64 = unsignedBase64 ** exponent

Precedence and associativity

Your shiny new custom operator seems to work just fine, but if you use it in a complex expression, Swift won’t know what to do with it:

2 * 2 ** 3 ** 2 // Does not compile!
2 * (2 ** (3 ** 2))
precedencegroup ExponentiationPrecedence {
  associativity: right
  higherThan: MultiplicationPrecedence
}

infix operator **: ExponentiationPrecedence
2 * 2 ** 3 ** 2

Subscripts

You’ve already used subscripts in Chapter 7, “Arrays, Dictionaries, Sets” to retrieve the elements of arrays and dictionaries. It’s high time you learned to create your very own subscripts. Think of them as overloading the [] operator to provide shortcuts for accessing elements of a collection, class, structure or enumeration.

subscript(parameterList) -> ReturnType {
  get {
    // return someValue of ReturnType
  }
 
  set(newValue) {
    // set someValue of ReturnType to newValue
  }
}
class Person {
  let name: String
  let age: Int

  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}
let me = Person(name: "Cosmin", age: 33)
me["name"]
me["age"]
me["gender"]
extension Person {
  subscript(key: String) -> String? {
    switch key {
    case "name":
      return name
    case "age":
      return "\(age)"
    default:
      return nil
    }
  }
}
me["name"]
me["age"]
me["gender"]
Cosmin
33
nil

Subscript parameters

You don’t have to use names for the subscript’s parameters when calling the subscript even if you don’t use underscores when declaring them. Add external parameter names if you want to be more specific like this:

subscript(key key: String) -> String? {
  // original code
}
me[key: "name"]
me[key: "age"]
me[key: "gender"]
subscript(property key: String) -> String? {
  // original code
}

me[property: "name"]
me[property: "age"]
me[property: "gender"]

Static subscripts

You can define static subscripts for custom types in Swift:

class File {
  let name: String
  
  init(name: String) {
    self.name = name
  }
  
  // 1
  static subscript(key: String) -> String {
    switch key {
    case "path":
      return "custom path"
    default:
      return "default path"
    }
  }
}

// 2
File["path"]
File["PATH"]

Dynamic member lookup

You use dynamic member lookup to provide arbitrary dot syntax to your type.

// 1
@dynamicMemberLookup
class Instrument {
  let brand: String
  let year: Int
  private let details: [String: String]
  
  init(brand: String, year: Int, details: [String: String]) {
    self.brand = brand
    self.year = year
    self.details = details
  }
  
  // 2
  subscript(dynamicMember key: String) -> String {
    switch key {
      case "info":
        return "\(brand) made in \(year)."
      default:
        return details[key] ?? ""
    }
  }
}

// 3
let instrument = Instrument(brand: "Roland", year: 2019, 
                            details: ["type": "acoustic", 
                                      "pitch": "C"])
instrument.info 
instrument.pitch
guitar.dlfksdf  // Returns ""
instrument.brand // "Roland"
instrument.year // 2019
class Guitar: Instrument {}
let guitar = Guitar(brand: "Fender", year: 2019, 
                    details: ["type": "electric", "pitch": "C"])
guitar.info
// 1
@dynamicMemberLookup
class Folder {
  let name: String
  
  init(name: String) {
    self.name = name
  }
  
  // 2
  class subscript(dynamicMember key: String) -> String {
    switch key {
      case "path":
        return "custom path"
      default:
        return "default path"
    }
  }
}

// 3
Folder.path
Folder.PATH

Keypaths

Keypaths enable you to store references to properties. For example, this is how you model the tutorials on our website:

class Tutorial {
  let title: String
  let author: Person
  let details: (type: String, category: String)
  
  init(title: String, author: Person, 
       details: (type: String, category: String)) {
    self.title = title
    self.author = author
    self.details = details
  }
}

let tutorial = Tutorial(title: "Object Oriented Programming in Swift", 
                        author: me, 
                        details: (type: "Swift", 
                                  category: "iOS"))
let title = \Tutorial.title
let tutorialTitle = tutorial[keyPath: title]
let authorName = \Tutorial.author.name
var tutorialAuthor = tutorial[keyPath: authorName]
let type = \Tutorial.details.type
let tutorialType = tutorial[keyPath: type]
let category = \Tutorial.details.category
let tutorialCategory = tutorial[keyPath: category]

Appending keypaths

You can make new keypaths by appending to existing ones like this:

let authorPath = \Tutorial.author
let authorNamePath = authorPath.appending(path: \.name)
tutorialAuthor = tutorial[keyPath: authorNamePath]

Setting properties

Keypaths can change property values. Suppose you set up your very own jukebox to play your favorite song:

class Jukebox {
  var song: String
  
  init(song: String) {
    self.song = song
  }
}

let jukebox = Jukebox(song: "Nothing Else Matters")
let song = \Jukebox.song
jukebox[keyPath: song] = "Stairway to Heaven"

Keypath member lookup

You can use dynamic member lookup for keypaths:

// 1
struct Point {
  let x, y: Int
}

// 2
@dynamicMemberLookup
struct Circle {
  let center: Point
  let radius: Int
  
  // 3
  subscript(dynamicMember keyPath: KeyPath<Point, Int>) -> Int {
    center[keyPath: keyPath]
  }
}

// 4
let center = Point(x: 1, y: 2)
let circle = Circle(center: center, radius: 1)
circle.x
circle.y

Challenges

Before moving on, here are some challenges to test your knowledge of custom operators, subscripts and keypaths. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Make it compile

Modify the following subscript implementation so that it compiles in a playground:

extension Array {
  subscript(index: Int) -> (String, String)? {
    guard let value = self[index] as? Int else {
      return nil
    }
 
    switch (value >= 0, abs(value) % 2) {
    case (true, 0):
      return ("positive", "even")
    case (true, 1):
      return ("positive", "odd")
    case (false, 0):
      return ("negative", "even")
    case (false, 1):
      return ("negative", "odd")
    default:
      return nil
    }
  }
}

Challenge 2: Random access string

Write a subscript that computes the character at a specific index in a string. Why is this considered harmful?

Challenge 3: Generic exponentiation

Implement the exponentiation generic operator for float types so that the following code works:

let exponent = 2
let baseDouble = 2.0
var resultDouble = baseDouble ** exponent
let baseFloat: Float = 2.0
var resultFloat = baseFloat ** exponent
let baseCG: CGFloat = 2.0
var resultCG = baseCG ** exponent

Challenge 4: Generic exponentiation assignment

Implement the exponentiation assignment generic operator for float types so that the following code works:

resultDouble **= exponent
resultFloat **= exponent
resultCG **= exponent

Key points

  1. Remember the custom operators mantra when creating brand new operators from scratch: With great power comes great responsibility. Make sure the additional cognitive overhead of a custom operator introduces pays for itself.
  2. Choose the appropriate type for custom operators: postfix, prefix or infix.
  3. Don’t forget to define any related operators, such as compound assignment operators, for custom operators.
  4. Use subscripts to overload the square brackets operator for classes, structures and enumerations.
  5. Use keypaths to create dynamic references to properties.
  6. Use dynamic member lookup to provide type safe dot syntax access to internal properties.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.