Overloading & Creating New Operators In Swift 5
We'll cover everything you need to know about operator overloading and creating custom operators with unique precedence and associativity behavior.
Operator overloading allows you to change how existing operators (e.g. +
, -
, *
, /
, etc.) interact with custom types in your codebase. Leveraging this language feature correctly can greatly improve the readability of your code.
Let’s say we had a struct to represent Money
:
struct Money {
let value: Int
let currencyCode: String
}
Now, imagine we’re building an e-commerce application. We’d likely need a convenient way of adding up the prices of all items in our shopping cart.
With operator overloading, instead of only being able to add numeric values together, we could extend the +
operator to support adding Money
objects together. Moreover, as part of this implementation, we could even add logic to support adding different currency types together!
In order to take advantage of this language feature, we just need to provide a custom implementation for the operator in our type's implementation:
struct Money {
let value: Int
let currencyCode: String
static func + (left: Money, right: Money) -> Money {
return Money(value: left.value + right.value,
currencyCode: left.currencyCode)
}
}
let shoppingCartItems = [
Money(value: 20, currencyCode: "USD"),
Money(value: 10, currencyCode: "USD"),
Money(value: 30, currencyCode: "USD"),
Money(value: 50, currencyCode: "USD"),
]
// Output: Money(value: 110, currencyCode: "USD")
print(shoppingCartItems.reduce(Money(value: 0, currencyCode: "USD"), +))
You may have even used operator overloading without realizing it. If you've ever implemented the Equatable
protocol, it requires you to provide a custom implementation for the ==
operator:
struct Money: Equatable {
let value: Int
let currencyCode: String
static func == (lhs: Money, rhs: Money) -> Bool {
lhs.currencyCode == rhs.currencyCode && lhs.value == rhs.value
}
}
I hope you'll agree that, in these examples, operator overloading has increased the code's readability and expressiveness. However, we should be careful not to overdo it.
Whenever you're creating or overriding an operator, make sure its use is obvious and undisputed. Language features like this often produce diminishing returns, as the more custom behavior we introduce, the harder it is for other developers to understand our code.
Creating Custom Operators
Whenever we're discussing custom operators, overloading an existing operator is always going to be the easier option. Assuming, of course, this doesn't compromise the code's legibility.
Instead, if we want to create our own operator, we'll need to specify 3 additional pieces of information: the type of the operator, the precedence order, and the associativity behavior.
When we're simply overloading an existing operator, we inherit all of this information from the parent operator directly.
Operator Types
When we want to create our own operator, we’ll need to specify whether it's of the prefix
, postfix
, or infix
variety.
prefix
- describes an operator that comes before the value it is meant to be used with (e.x. !isEmpty
)
postfix
- describes an operator that comes after the value it is meant to be used with (e.x. the force-unwrapping operator - user.firstName!
)
prefix
andpostfix
are also referred to as "unary” operators as they only affect a single value.
infix
- describes an operator that comes in between the value it is meant to be used with and is the most common type (e.x. +
, -
, /
, *
are all infix
operators)
infix
are also referred to as "binary” operators since they operate on two values.
Precedence
For the statement - 2 + 5 x 5
- we know that the answer is 2 + (5 x 5) => 27
because the precedence of the operators involved tell us the order in which to evaluate the expression.
In the same way that the higher precedence of multiplication resolves the ambiguity in the order of operations here, we need to provide the compiler with similar information when we create a custom operator.
By specifying the precedence, we can control the order in which the expression is evaluated as operations belonging to a higher precedence group are always evaluated first.
We'll see how to specify the precedence of our operator shortly, but let's understand the current state of affairs in Swift first.
The following image shows a list of all precedence group types in Swift from the highest priority to the lowest priority:
If you declare a new operator without specifying a precedence group, it is a member of the DefaultPrecedence
precedence group which has no associativity.
Associativity
The associativity of an operator is simply a property that specifies how operators of the same precedence level are grouped in the absence of parentheses.
Imagine we have an expression with multiple operators all belonging to the same precedence level:
20 / 2 / 5
We could process this expression in 2 different ways:
(20 / 2) / 5
or
20 / (2 / 5)
which would give us 2
and 50
, respectively.
This ambiguity is exactly what the operator's associativity helps us resolve.
An operator can be associative (meaning the operations can be grouped arbitrarily), left-associative (meaning the operations are grouped from the left), and right-associative (meaning the operations are grouped from the right).
In the simplest terms, when we say an operator is left-associative we simply evaluate our expression from left to right. Conversely, for a right-associative operator, we evaluate our expression from right to left.
As another example, we know that *
, /
, and %
all have the same precedence, but by changing their associativity, we can get wildly different results:
Left-Associative
(4 * 8) / 2 % 5 ==> (32 / 2) % 5 ==> 16 % 5 ==> 1
Right-Associative
4 * 8 /(2 % 5) ==> 4 * ( 8 / 2) ==> 4 * 4 ==> 16
Put differently, operator associativity allows us to specify how an expression should be evaluated when it involves multiple operators of the same precedence group.
All arithmetic operators are left-associative.
In order for expressions involving our custom operator to evaluate correctly, we'll need to be mindful of both the operator's precedence and associativity behavior.
Creating A Custom Operator
With all of the theory out of the way, let's create a custom operator that will allow us to easily perform exponentiation.
Since exponentiation has a higher precedence than multiplication and is right-associative, we'll need to create a new precedence group as this operation doesn't match any of Swift's existing precedence group options.
We'll create the precedencegroup
by filling in the relevant fields from this template:
// Precedence Group Template
precedencegroup GROUP_NAME {
lowerThan: OTHER_GROUP_NAME
higherThan: OTHER_GROUP_NAME
associativity: LEFT_OR_RIGHT_OR_NONE
assignment: TRUE_OR_FALSE
}
precedencegroup ExponentPrecedentGroup {
higherThan: MultiplicationPrecedence
associativity: right
}
Next, we need to let the compiler know about the existence of our custom operator and specify its behavior:
infix operator ^^ : ExponentPrecedentGroup
func ^^ (lhs: Double, rhs: Double) -> Double {
// We can omit the return since it's a one-line function
pow(lhs, rhs)
}
And now, anywhere else in our code we're free to use our custom operator:
2 ^^ 8 => 256.0
It's important to mention here that our precedence group declaration and all operator declarations and functions must be placed at the file scope - outside of any enclosing type.
Don't worry if you forget this, the compiler will dutifully remind you.
Limitations Of Operator Overloading
There are a few additional caveats to mention.
While ternary operator types (e.g. var userStatus = user.age > 18 ? .approved : .rejected
) also exist in Swift, the language does not currently allow for overloading their operation.
This same restriction applies to the default assignment operator (=
) and the compound assignment operator ( +=
).
Otherwise, all operators that begin with /
, =
, -
, +
, !
, *
, %
, <
, >
, &
, |
, ^
, ?
, or ~
, or are one of the Unicode characters specified here are fair game.
Best Practices
While this language feature is extremely powerful and go a long way towards improving your code's legibility and friendliness, it can also take you in the opposite direction.
Let's take a moment to discuss some best practices around using this language feature.
Firstly, overloading operators in Swift should be done in a way that is consistent with how the operator is normally used. A new developer should be able to reason about the expected behavior of the operator without needing to check the implementation.
Next, in situations where the traditional operators don't make semantic sense, you may want to consider creating a custom operator instead.
Finally, and a more pragmatic point, they should be easy to remember and type on the keyboard - an obscure custom operator like .|.
benefits no one as its meaning is neither intuitive nor is it convenient to type.
Ultimately, this is just a long-winded way of saying that custom operators, typealias
, and all other forms of "syntactic sugar" can improve your code's clarity and your development speed when used with a bit of restraint and pragmatism.
If you're interested in more articles about iOS Development & Swift, check out my YouTube channel or follow me on Twitter.
Join the mailing list below to be notified when I release new articles!
Do you have an iOS Interview coming up?
Check out my book Ace The iOS Interview!
Further Reading
Want to take a deeper dive into operator overloading?
Check out these great resources: