Operators are the core building blocks of any programming language. Can you imagine programming without using +
or =
?
Operators are so fundamental that most languages bake them in as part of their compiler (or interpreter). The Swift compiler, on the other hand, doesn’t hardcode most operators but instead provides libraries a way to create their own. It leaves the work up to the Swift Standard Library to provide all of the common ones you’d expect. This difference is subtle but opens the door for tremendous customization potential.
Swift operators are particularly powerful because you can alter them to suit your needs in two ways: assigning new functionality to existing operators (known as operator overloading), and creating new custom operators.
A simple example of operator overloading is the addition operator. If you use this operator with two integers, the following happens:
1 + 1 // 2
But if you use the same addition operator with strings, it behaves a little differently:
"1" + "1" // "11"
When +
is used with two integers, it adds them arithmetically. But when it’s used with two strings, it concatenates them.
In this tutorial, you’ll explore how you can mold operators to your own needs and build your own 3D vector type in Swift.
Getting Started
Begin by opening Xcode. Create a new playground, name it Operators and select the iOS platform. Delete all the default code so you can start with a blank slate.
Add the following code to your playground:
import UIKit
struct Vector: ExpressibleByArrayLiteral, CustomStringConvertible {
let x: Int
let y: Int
let z: Int
var description: String {
return "(\(x), \(y), \(z))"
}
init(_ x: Int, _ y: Int, _ z: Int) {
self.x = x
self.y = y
self.z = z
}
init(arrayLiteral: Int...) {
assert(arrayLiteral.count == 3, "Must initialize vector with 3 values.")
self.x = arrayLiteral[0]
self.y = arrayLiteral[1]
self.z = arrayLiteral[2]
}
}
Here you define a new Vector
type with three properties and two initializers. The CustomStringConvertible
protocol and the description
computed property let you print the vector as a friendly String
.
At the bottom of the playground, add the following lines:
let vectorA: Vector = [1, 3, 2]
let vectorB: Vector = [-2, 5, 1]
The ExpressibleByArrayLiteral
protocol provides a frictionless interface to initialize a Vector
. The protocol requires a non-failable initializer with a variadic parameter: init(arrayLiteral: Int…)
.
Here’s what that means: The variadic parameter ...
lets you pass in an unlimited number of values separated by commas. For example, you can create a Vector
like Vector(0)
or Vector(5, 4, 3)
.
The protocol takes convenience a step further and allows you to initialize with an array directly, which is what you’ve done in let vectorA: Vector = [1, 3, 2]
.
The only caveat to this approach is that you have to accept arrays of any length. If you put this code into an app, keep in mind that it will crash if you pass in an array with a length other than exactly three. The assertion at the top of the initializer will alert you in the console during development and internal testing if you ever try to initialize a Vector
with less than or more than three values for the dimensions.
Vectors alone are nice, but it would be even better if you could do things with them. Just as you did in grade school, you’ll start your learning journey with addition.
Overloading the Addition Operator
In order to overload an operator, you have to implement a function whose name is the operator symbol.
static
so it is accessible without an instance of the type it is defined within.Add the following function at the end of the Vector
implementation, just before the closing curly brace:
static func +(left: Vector, right: Vector) -> Vector {
return [left.x + right.x, left.y + right.y, left.z + right.z]
}
This function will take two vectors in and return the sum vector. To add vectors, you simply need to add each individual component.
Now, to test this function, add the following to the bottom of the playground:
vectorA + vectorB // (-1, 8, 3)
You can see the resultant vector in the right-hand sidebar in the playground.
Other Types of Operators
The addition operator is what is known as an infix operator, meaning that it is used between two different values. There are other types of operators as well:
– infix
: Used between two values, like the addition operator (e.g. 1 + 1
)
– prefix
: Added before a value, like the negative operator (e.g. -3
).
– postfix
: Added after a value, like the force-unwrap operator (e.g. mayBeNil!
)
– ternary
: Two symbols inserted between three values. In Swift, user defined ternary operators are not supported and there is only one built-in ternary operator which you can read about in Apple’s documentation.
The next operator you’ll want to overload is the negative sign, which will take vectorA
, which is (1, 3, 2), and return (-1, -3, -2).
Add this code to the end of the Vector
implementation:
static prefix func -(vector: Vector) -> Vector {
return [-vector.x, -vector.y, -vector.z]
}
Operators are assumed to be infix, so if you want your operator to be a different type, you’ll need to specify the operator type in the function declaration. The negation operator is not infix, so you added the prefix
modifier to the function declaration.
At the bottom of the playground, add the line:
-vectorA // (-1, -3, -2)
Check for the correct result in the sidebar.
Next is subtraction, which I will leave to you to implement yourself. When you finish, check to make sure your code is similar to mine. Hint: subtraction is the same thing as adding a negative.
Give it a shot, and if you need hep, check the solution below!
Now test the new operator out by adding this code to the bottom of the playground:
vectorA - vectorB // (3, -2, 1)
Mixed Parameters? No Problem!
You can also multiply vectors by a number through scalar multiplication. To multiply a vector by two, you multiply each component by two. You’re going to implement this next.
One thing you need to consider is the order of the arguments. When you implemented addition, order didn’t matter because both parameters were vectors.
For scalar multiplication, you need to account for Int * Vector
and Vector * Int
. If you only implement one of these cases, the Swift compiler will not automatically know that you want it to work in the other order.
To implement scalar multiplication, add the following two functions to the end of the implementation for Vector
:
static func *(left: Int, right: Vector) -> Vector {
return [right.x * left, right.y * left, right.z * left]
}
static func *(left: Vector, right: Int) -> Vector {
return right * left
}
To avoid writing the same code multiple times, you simply write the second function using the first.
In mathematics, vectors have another interesting operation known as the cross-product. How cross-products work is beyond the scope of this tutorial, but you can learn more about it on the Cross_product Wikipedia page.
Because using custom symbols is discouraged in most cases (who wants to open the Emoji menu while coding?), it would be very convenient to reuse the asterisk for cross-product.
Cross-products, unlike scalar multiplication, take two vectors as parameters and return a new vector.
Add the following code to add the cross-product implementation at the bottom of the Vector
implementation:
static func *(left: Vector, right: Vector) -> Vector {
return [left.y * right.z - left.z * right.y, left.z * right.x - left.x * right.z, left.x * right.y - left.y * right.x]
}
Now, add the following calculation to the bottom of the playground:
vectorA * 2 * vectorB // (-14, -10, 22)
This code finds the scalar multiple of vectorA
and 2, then finds the cross-product of that vector with vectorB
. Note that the asterisk operator always goes from left to right, so the previous code is the same as if you had used parentheses to group the operations, like (vectorA * 2) * vectorB
.
Protocol Operators
Some protocols are required members of protocols. For example, a type that conforms to Equatable
must implement the ==
operator. Similarly, a type that conforms to Comparable
must implement at least <
(and optionally >
, >=
, and <=
)
For Vector
, Comparable
doesn't really make a lot of sense, but Equatable
does, since two vectors are equal if their components are all equal. You’ll implement Equatable
next.
To conform to the protocol, add this to the end of the declaration for Vector
:
struct Vector: ExpressibleByArrayLiteral, CustomStringConvertible, Equatable {
Xcode will now be yelling at you that Vector
does not conform to Equatable
. That's because you haven't yet implemented ==
. To do that, add the following static function to the bottom of the Vector
implementation:
static func ==(left: Vector, right: Vector) -> Bool {
return left.x == right.x && left.y == right.y && left.z == right.z
}
Add the following line to the bottom of your playground to test this out:
vectorA == vectorB // false
This line returns false
as expected because vectorA
has different components than vectorB
.
Creating Custom Operators
Remember how I said that using custom symbols is usually discouraged? As always, there are exceptions to the rule.
A good way to think about custom symbols is that they should only be used if the following are true:
- Their meanings are well-known or would make sense to someone reading the code.
- They are easy to type on the keyboard.
This last operator you will implement matches both of these conditions. The vector dot-product takes two vectors and returns a single scalar number. This is done by multiplying each value in a vector by its counterpart in the other vector, and adding up all these products.
The symbol for dot-product is •
, which can be easily typed using Option-8 on your keyboard.
You might be thinking, "I can just do the same thing I did with every other operator in this tutorial, right?"
Unfortunately, you can't do that just yet. In the other cases, you are overloading an operator that already exists. For new custom operators, you need to create the operator first.
Directly underneath the Vector
implementation, but above the line starting with let vectorA...
, add the following declaration:
infix operator •: AdditionPrecedence
This defines •
as an operator that must be placed between two other values and has the same precedence as the addition +
operator. Ignore precedence just for the moment because you will come back to it.
Now that this operator has been registered, add its implementation to the Vector body:
static func •(left: Vector, right: Vector) -> Int {
return left.x * right.x + left.y * right.y + left.z * right.z
}
Add the following code to the bottom of the playground to test this out:
vectorA • vectorB // 15
Everything looks good so far...or does it? Try the following code at the bottom of the playground:
vectorA • vectorB + vectorA // Error!
Xcode isn't very happy with you. But why?
Right now, •
and +
have the same precedence, so the compiler parses the expression from left to right. Your code is interpreted as:
(vectorA • vectorB) + vectorA
This expression boils down to Int + Vector
, which you haven’t implemented, and don't plan to implement. What can you do to fix this?
Precedence Groups
All operators in Swift belong to a precedence group, which describes the order in which operators should be evaluated. Remember learning PEMDAS in elementary school math? That is basically what you are dealing with here.
In the Swift standard library, the order of precedence is as follows:
Here are a few notes about these operators, since you may not have seen them before:
- Bitwise shift operators,
<<
and>>
, are used for binary calculations. - You use casting operators,
is
andas
, to determine or change a value's type. - The nil coalescing operator,
??
, helps convert optionals into non-optionals. - If your custom operator does not specify a precedence, default precedence is automatically assigned.
- The ternary operator,
? :
, is analogous to an if-else statement. AssignmentPrecedence
, for the derivatives of=
, is evaluated after everything else no matter what.
Types that have a left associativity are parsed so that v1 + v2 + v3 == (v1 + v2) + v3
. The opposite is true for right associativity.
Operators are parsed in the order they appear in the table. Try to rewrite the following code using parentheses:
v1 + v2 * v3 / v4 * v5 == v6 - v7 / v8
When you're ready to check your math, look at the solution below.
In most cases, you'll want to add parentheses to make your code easier to read. Either way, it's useful to understand the order in which the compiler evaluates operators.
Dot Product Precedence
Your new dot-product doesn’t really fit into any of these categories. It has to be less than addition (as you realized before), but does it really fit into CastingPrecedence
or RangeFormationPrecedence
?
Instead, you are going to make your own precedence group for your dot-product operator.
Replace your original declaration of the •
operator with the following:
precedencegroup DotProductPrecedence {
lowerThan: AdditionPrecedence
associativity: left
}
infix operator •: DotProductPrecedence
Here, you create a new precedence group named DotProductPrecedence
. You place it lower than AdditionPrecedence
because you want addition to take precedence. You also make it left-associative because you want it evaluated from left-to-right as you do in addition and multiplication. Then you assign your •
operator to this new precedence.
lowerThan
, you can also specify higherThan
in your DotProductPrecedence
. This becomes important if you have multiple custom precedence groups in a single project.Your old line of code now runs and returns as expected:
vectorA • vectorB + vectorA // 29
Congratulations — you've mastered custom operators!
Where to Go From Here?
Here is the finished playground from this tutorial.
At this point, you know how to bend Swift operators to your needs. In this tutorial, you focused on using operators in a mathematical context. In practice, you’ll find many more ways to use operators.
A great demonstration of custom operator usage can be seen in the ReactiveSwift framework. One example is <~
for binding, an important function in reactive programming. Here is an example of this operator in use:
let (signal, observer) = Signal.pipe()
let property = MutableProperty(0)
property.producer.startWithValues {
print("Property received \($0)")
}
property <~ signal
Cartography is another framework that heavily uses operator overloading. This AutoLayout tool overloads the equality and comparison operators to make NSLayoutConstraint
creation simpler:
constrain(view1, view2) { view1, view2 in
view1.width == (view1.superview!.width - 50) * 0.5
view2.width == view1.width - 50
view1.height == 40
view2.height == view1.height
view1.centerX == view1.superview!.centerX
view2.centerX == view1.centerX
view1.top >= view1.superview!.top + 20
view2.top == view1.bottom + 20
}
Additionally, you can always reference the official custom operator documentation from Apple.
Armed with these new sources of inspiration, you can go out into the world and make your code simpler with operator overloading. Just be careful not to go too crazy with custom operators! :]
If you have any questions or comments on this tutorial, please join the discussion below!
The post Overloading Custom Operators in Swift appeared first on Ray Wenderlich.