2.
Custom Operators, Subscripts & Keypaths
Written by Ehab Amer
In “Swift Apprentice: Fundamentals”, you learned the basics of operator overloading where you implemented the Equatable and Comparable protocols and added custom behavior to standard operators.
However, there are certain cases when overloading standard operators isn’t sufficient. This chapter will show you how to create custom operators from scratch and define your own subscripts. You’ll use subscripts as shortcuts for accessing the elements of custom types and provide keypaths as dynamic references to properties in your types.
Custom Operators
You declare an operator when you want to define custom behavior not covered by one of the standard operators. Think of exponentiation, for example. You could overload the multiplication operator since exponentiation means repeated multiplication. But this would be confusing. Operators should do only one type of operation, not two.
You’ll define an exponentiation operator, first only for a specific type, then extend it by making it generic. Before doing that, you need to know some theory about operator types. Time to dive in!
Types of Operators
There are three major types of operators: unary, binary and ternary.
- Unary operators work with only one operand and are defined either as postfix if they appear after the operand or prefix if they appear before the operand. The logical-not operator (!) is a unary prefix operator, and the force unwrapping operator (also !) is a unary postfix one. You learned about them in “Swift Apprentice: Fundamentals - Chapter 3: Basic Control Flow” and “Chapter 6: Optionals”.
-
Binary operators work with two operands and are infix because they appear between them. All the arithmetic operators (
+,-,*,/,%), comparison operators (==,!=,<,>,<=,>=) and most of the logical ones (&&,||) are binary infix. -
Ternary operators work with three operands. This operator (
?:) is the only ternary operator in Swift, and you can’t make your own.
Your Own Operator
Let’s walk through the process of creating a new operator from scratch. You’ll create one for exponentiation. Since it’s a custom one, you can 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, choosing something that reflects that is good. You’ll use ** since other languages also use this notation.
Now for the operator’s type. The ** operator works with two operands, an infix (binary) operator.
Here’s what the operator’s signature looks like:
infix operator **
Nothing fancy here: the operator’s name and type are bundled into one line of code with the operator keyword. As for the operator’s implementation, a naive one looks like this:
func **(base: Int, power: Int) -> Int {
precondition(power >= 2)
var result = base
for _ in 2...power {
result *= base
}
return result
}
The function takes two arguments of type Int and uses loops, ranges and wildcards to return the first argument raised to the power of the second one. Note the multiplication assignment operator in action.
Note: You use the wildcard pattern to discard the loop’s values. You’ll learn more about it and other pattern matching techniques in “Chapter 4: Pattern Matching”.
Now test your brand-new operator:
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
}
The operator’s name is **= and its infix, just like the exponentiation operator created earlier. It has no return type and instead uses the inout keyword in front of the type of the operand you are modifying. You’ve already seen inout in action in “Swift Apprentice: Fundamentals - Chapter 5: Functions”. A function can mutate parameters marked with inout.
Here is how the operator works:
var number = 2
number **= exponent
The value number is modified by the **= operator in-place.
Mini-exercises
- Implement a custom multiplication operator for strings so that the following code works:
let baseString = "abc"
let times = 5
var multipliedString = baseString ** times
- Implement the corresponding multiplication assignment operator so that the following code runs without errors:
multipliedString **= times
Generic Operators
You want the exponentiation operator to work for all 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
}
Notice the BinaryInteger type constraint on the generic parameter. This constraint is required here as the *= operator used in the function body isn’t available on any type T. However, it’s available on all types that conform to the BinaryInteger protocol. The function’s body is the same as before since the generic operator does the same thing as its non-generic equivalent.
Your previous code should still work. Now that the operator is generic, test it with some types other than Int:
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
The exponentiation operator now works for all integer types: Int, UInt, Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64 and UInt64.
Note: You can also use the
pow(_ base: Double, _ exponent: Double)function from the operating system (Darwin on the Mac) for exponentiation, but it doesn’t work for all the above types. However, it does handle negative and fractional exponents and is O(log) instead of O(n) as in the naive implementation.
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!
To understand this expression, Swift needs the following information about your operator:
- Precedence: Should the multiplication be done before or after the exponentiation?
- Associativity: Should the consecutive exponentiations be done left to right or right to left?
Without this information, the only way to get Swift to understand your code is to add parentheses.
2 * (2 ** (3 ** 2))
These parentheses tell Swift that the exponentiation happens before the multiplication and from right to left. If this is always the case, you can define this behavior using a precedence group.
Change your operator definition to the following:
precedencegroup ExponentiationPrecedence {
associativity: right
higherThan: MultiplicationPrecedence
}
infix operator **: ExponentiationPrecedence
Here, you’re creating a precedence group for your exponentiation operator, telling Swift it’s right-associative and has higher precedence than multiplication.
Swift will now understand your expression, even without parentheses:
2 * 2 ** 3 ** 2
Maybe that is a good thing and perhaps it’s not. You may choose to make associativity: none and force users to make things explicit with parenthesis.
That’s it for custom operators. Time for some fun with subscripts!
Subscripts
You’ve already used subscripts in “Swift Apprentice: Fundamentals - 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.
The subscript syntax is as follows:
subscript(parameterList) -> ReturnType {
get {
// return someValue of ReturnType
}
set(newValue) {
// set someValue of ReturnType to newValue
}
}
As you can see, subscripts behave like functions and computed properties:
-
The subscript’s prototype looks like a function’s signature: It has a parameter list and a return type, but instead of the
funckeyword and the function’s name, you use thesubscriptkeyword. Subscripts may have variadic parameters and can throw errors but can’t useinoutor default parameters. You’ll learn more about errors in “Chapter 5: Error Handling”. -
The subscript’s body looks like a computed property: It has a getter and a setter. The setter is optional, so the subscript can be either read-write or read-only. You can omit the setter’s
newValuedefault parameter; its type is the same as the subscript’s return type. Only declare it if you want to change its name to something else.
Enough theory! Add a subscript to a Person class defined as follows:
class Person {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
The Person class has two stored properties: name of type String and age of type Int, along with a designated initializer to kick things off.
Now suppose I want to create a version of myself right now, as follows:
let me = Person(name: "Ehab", age: 37)
It would be nice to access my characteristics with a subscript like this:
me["name"]
me["age"]
me["gender"]
If you run this, Xcode will output the following error:
Type "Person" has no subscripts members
Whenever you use the square brackets operator, you call a subscript method under the hood. Your class has no subscripts defined by default, so you have to declare them yourself.
Add the following code to the Person class with an extension like this:
extension Person {
subscript(key: String) -> String? {
switch key {
case "name": return name
case "age": return "\(age)"
default: return nil
}
}
}
The subscript returns an optional string based on the key you provide: You return the key’s corresponding property value or nil if you don’t use a valid key. The switch must be exhaustive, so you need a default case.
The subscript is read-only, so its entire body is a getter — you don’t need to state that with the get keyword explicitly.
The above test code works now:
me["name"]
me["age"]
me["gender"]
And outputs:
Ehab
37
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
}
The parameter’s name appears in the subscript call now:
me[key: "name"]
me[key: "age"]
me[key: "gender"]
Use descriptive names for external parameters instead of their local counterparts if you want to add more context to the subscript:
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"]
The code works like this:
- Use
staticto create a static subscript that returns the default or custom path forFile. - Call the subscript on
Fileinstead of aFileinstance.
Dynamic Member Lookup
You use dynamic member lookup to provide arbitrary dot syntax to your type.
Consider the following:
// 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: 2021,
details: ["type": "acoustic", "pitch": "C"])
instrument.info
instrument.pitch
Going through the above code step by step:
- Mark
Instrumentas@dynamicMemberLookupto enable dot syntax for its subscripts. - Conform
Instrumentto@dynamicMemberLookupby implementingsubscript(dynamicMember:). - Call the previously implemented subscript using dot syntax. It returns either contents from
detailsor more information aboutInstrument.
Using @dynamicMemberLookup here makes the contents of the details dictionary available as properties, which improves readability.
However, the compiler evaluates dynamic member calls at runtime, so you lose the usual compile-time safety. For example, this compiles without complaint:
guitar.dlfksdf // Returns ""
You should use @dynamicMemberLookup judiciously as it can prevent the compiler from checking an entire class of errors that it could previously identify at compile time. You can compose this feature with keypaths that you’ll learn about in a moment to maintain type safety and prevent the above nonsense.
This code also works:
instrument.brand // "Roland"
instrument.year // 2021
A derived class inherits dynamic member lookup from its base one:
class Guitar: Instrument {}
let guitar = Guitar(brand: "Fender",
year: 2021,
details: ["type": "electric", "pitch": "C"])
guitar.info
You use dot syntax to call the Guitar subscript since Guitar is an Instrument and Instrument implements @dynamicMemberLookup.
You may also use dynamic member lookup for class subscripts in Swift. They behave like static subscripts, and you can override them in subclasses:
// 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
Here’s what’s going on over here:
- Mark
Folderas@dynamicMemberLookupto enable dot syntax for custom subscripts. - Use
classand dynamic member lookup to create a class subscript that returns the default or custom path forFolder. - Call the subscript on
Folderwith dot syntax.
Subscripts are easy to use and implement and live somewhere between computed properties and methods. However, take care to use them sparingly. Unlike computed properties and methods, subscripts have no name to make their intentions clear. Subscripts are almost exclusively used to access a collection’s elements, so don’t confuse the readers of your code by using them for something unrelated and unintuitive!
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"))
Each tutorial has a certain title, author, type and category. Using keypaths, you can get the tutorial’s title like this:
let title = \Tutorial.title
let tutorialTitle = tutorial[keyPath: title]
You first use a backslash \ to create a keypath for the title property of the Tutorial class and then access its corresponding data with the keyPath(_:) subscript.
Keypaths can access properties several levels deep:
let authorName = \Tutorial.author.name
var tutorialAuthor = tutorial[keyPath: authorName]
You can also use keypaths for tuples in Swift:
let type = \Tutorial.details.type
let tutorialType = tutorial[keyPath: type]
let category = \Tutorial.details.category
let tutorialCategory = tutorial[keyPath: category]
Here you use keypaths to get type and category from details in tutorial.
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]
You use the appending(path:) method to add a new keypath to the already defined authorPath and infer the keypath’s base type.
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")
You declare the song property as a variable because your best friend comes to visit and wants to listen to their favorite song instead:
let song = \Jukebox.song
jukebox[keyPath: song] = "Stairway to Heaven"
You use the song keypath to change the song for your friend, and everyone is happy now!
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
Here’s what this code does:
- Declare a type
Pointwithxandycoordinates. - Annotate
Circlewith@dynamicMemberLookupto enable dot syntax for its subscripts. - Create a subscript that uses keypaths to access
centerproperties fromCircle. - Call
centerproperties oncircleusing dynamic member lookup.
As you can see, using keypaths is more involved than using properties. With keypaths, accessing a property becomes a two-step process:
- First, you decide which property you need and create a keypath.
- Then, you pass this keypath to an instance using the keypath subscript to access the selected property.
By using dynamic member lookup with keypaths, you maintain type safety. In other words, you can access circle.x and circle.y, but circle.z and circle.sdlkfj don’t compile! This type safety makes these two language features a powerful combo.
Note: The SwiftUI framework uses dynamic member lookup with keypaths to automatically wrap your properties inside other types that manage the
Viewstate and redrawing updates. You may not even realize your type is being used this way because you can access all of its properties as you normally would.
Keypaths as Functions
You can use keypaths as functions if the function is a closure with only one parameter and the keypath’s returned type matches the returned type of the closure:
let anotherTutorial = Tutorial(title: "Encoding and Decoding in Swift",
author: me,
details: (type: "Swift", category: "iOS"))
let tutorials = [tutorial, anotherTutorial]
let titles = tutorials.map(\.title)
Here you use the title keypath to map tutorials to their titles.
Challenges
Before moving on, here are some challenges to test your custom operators, subscripts and keypaths knowledge. It’s best to try and 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
Hint: Import the CoreGraphics framework to work with CGFloat.
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
- 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.
- Choose the appropriate type for custom operators:
postfix,prefixorinfix. - Don’t forget to define any related operators, such as compound assignment operators, for custom operators.
- Use subscripts to overload the square brackets operator for classes, structures and enumerations.
- Use keypaths to create dynamic references to properties.
- Use dynamic member lookup to provide type-safe dot syntax access to properties.