Facets of Swift, Part 5: Custom Operators

Tammo Freese
Swift Programming
Published in
15 min readOct 5, 2014

Facets of Swift is a series of articles on various aspects of the Swift programming language. At last I have found the time to write the fifth article of this series. As you may have guessed from the title, it’s about custom operators. Operators are closely related to functions, which were the topic of the previous article.

What’s an Operator?

Most programming languages use operators. Simply put, an operator is a symbol or phrase that represents a functionality. For example, i = 1 in Swift uses the assignment operator = which has the functionality of storing the value 1 in the variable i. Many languages also have phrases that are operators, like sizeof in C, and the type-cast operator as in Swift.

Arity

An operator, or more accurately the operation represented by an operator, accepts a number of operands (parameters in operator lingo). The number of operands an operator accepts is called the arity. From Objective-C, we know operators with arity 1–3.

Unary operators represent operations with one operand. Examples in Objective-C include the negation operator !, the increment operator ++, and the sizeof operator.

Binary operators represent operations with two operands. The majority of Objective-C’s operators are binary operators, like +, -, *, /, <, <=, >=, >, ==, !=, &&, =, +=, -=, and so on.

Ternary operators represent operations with three operands. In Objective-C and Swift, there is only one ternary operator: The conditional expression ? : . As it’s the only ternary operator in both languages, and as we can’t customize it, let’s focus on unary and binary operators from now on.

Placement

An operator can be placed in three ways relative to its operands. An unary operator can be placed on either side of the operand: Either as a prefix, like the negation operator in !visible, or as a postfix, like the increment operator in i++.

Binary operators could be placed as a prefix, postfix, or infix (between the operands). In Objective-C and Swift, binary operators are always placed infix. As we focus on unary and binary operators, the placement (also called fixity) of an operator is sufficient to determine the arity: Prefix and postfix operators are unary, infix operators are binary.

Overloading

Operators are often overloaded, meaning they work for different types. In C/Objective-C for instance, the + operator is overloaded to allow adding two int and two double, among others. In contrast, functions in C/Objective-C are never overloaded. To be fair, the C11 standard introduced a _Generic keyword which allows us to at least write a macro that emulates function overloading.

Precedence and Associativity

Operators have a precedence, and an associativity that determine the order in which they are executed.

let a = 3
let b = 4
let c = a * a + b * b
let d = b  a + b

Swift follows the rules we are used to from math. Multiplication has a higher precedence than addition. So the first calculation results in c = (a * a) + (b * b) = 25. Addition and subtraction have the same precedence, and left associativity. So the calculation is grouped from left to right, and results in d = (b - a) + b = 5.

Operators can either be left-associative, right-associative, or non-associative. You may wonder how non-associativity may be a good thing. Here is an example:

BOOL a = NO;
BOOL b = NO;
BOOL c = YES;
if (a == b == c) {
NSLog(@"WAT");
}

At first glance, we may expect this code to only print “WAT” if and only if a, b, and c are equal. However, == is left-associative in Objective-C, so a == b == c means (a == b) == c, which is true for a couple of combinations.

In Swift, == is non-associate, so the compiler will gives us a shiny error that a non-associative operator is adjacent to an operator of the same precedence.

var a = false
var b = false
var c = true
if a == b == c { // COMPILER ERROR
println("WAT")
}

I like it when a language keeps me from making stupid mistakes.

Short-Circuit Evaluation

A couple of operators in Objective-C and Swift provide short-circuit evaluation. That means an operand is only calculated if necessary. The following code will not print anything, as the result of the logical && operator is already determined by the first operand.

func foo() -> Bool {
println("foo() called")
return true
}
false && foo()

Now that we have the terminology straight (unary, binary, prefix, postfix, infix, precedence, associativity, short-circuit evaluation), let’s see how we add operators in Swift.

Adding Operators in Swift

Objective-C does not allow us to add operators: They are hard-coded in the language. In Swift, we can define a surprisingly rich set of operators. We can use most combinations of the ASCII symbols ., +, -, *, /, %, <, =, >, !, &, |, ^, ~. Even better, we can use a plethora of Unicode characters. Math. Arrows. Symbols. Dingbats. And more. And if that’s not enough, we can use Unicode combining characters on those characters as well.

Let’s add the operators logical negation ¬, logical conjunction , and logical disjunction as alternatives to !, &&, and ||.

Operator Declaration

Adding a new operator consists of two parts: We have to declare the operator, and to implement at least one function giving meaning to the operator. The logic operator ¬ is a prefix operator:

prefix operator ¬ {}

Associativity and precendence declarations go between the braces. Unary operators in Swift always take precedence over binary operators, so we aren’t allowed to specify any precedence. We are also not allowed to specify an associativity, as unary operators always cling to their operand. As a result, whenever we add a prefix or postfix operator, the braces remain empty — but they have to be there.

Operator Implementation

Next, we implement a function for the operator. The function name is simply the operator symbol. We have to put prefix in front of the function declaration, so the compiler knows which function to use even if we would later declare a postfix version of the operator as well (think ++i vs i++).

prefix func ¬(a: Bool) -> Bool {
return !a
}

That’s it! Our new operator is ready for use:

assert(¬false)
assert(¬(¬true))

Infix Operators

Next up: The operator. A first attempt by copy-pasting and adapting the ¬ operator may look like this:

infix operator  {}
infix func (lhs: Bool, rhs: Bool) -> Bool { // COMPILER ERROR
return lhs && rhs
}

The compiler complains that the infix modifier is not required or allowed on function declarations. OK, let’s delete it. Then the code works — as long as we don’t try to use the operator in an expression more than once.

assert(true ∧ true)
assert(¬(true ∧ false))
assert(true ∧ true ∧ true) // COMPILER ERROR

Choosing an Associativity

That’s because for infix operators, the default associativity is none. The && operator for which our operator will be an alternative is left associative. So we declare our operator to be left associative as well, and the error goes away.

infix operator ∧ { associativity left }

Adding Short-Circuit Evaluation

Now let’s add short-circuit evaluation. The following code should not print anything.

func foo() -> Bool {
println(“foo() called”)
return true
}
false ∧ foo()

But of course it does: false ∧ foo() differs to a function call syntactically, but is the same semantically. We can even write down the function call:

(∧)(false, foo())

The parentheses around the operator symbol are needed as ∧(false, foo()) would be parsed as using a prefix operator with the operand (false, foo()).

All arguments of a function call are evaluated before the call takes place. How can we delay the evaluation of the second parameter? We could make the second parameter a closure returning a Bool. If you don’t know what a closure is, think “anonymous function” or “Objective-C block-like thingy”, both are close enough to understand what’s going on.

func ∧(lhs: Bool, rhs: () -> Bool) -> Bool {
return lhs && rhs()
}
func foo() -> Bool {
println(“foo() called”)
return true
}
false ∧ { foo() }
(^)(false, { foo() })

Now rhs() will only be evaluated if lhs is true, because of the short-circuit evaluation of &&. But this comes at a price: We have to put the second argument in braces ourselves now to make it a closure. What we need is something that automatically wraps the second parameter in a closure. In Swift, that can be done by using the @autoclosure attribute on the parameter. Now we get real short-circuit evaluation, without having to wrap the second argument in braces.

func ∧(lhs: Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs && rhs()
}
func foo() -> Bool {
println(“foo() called”)
return true
}
false ∧ foo()
(^)(false, foo())

Allow me to get sidetracked here for a bit. As the last line of the example above demonstrates, the autoclosure attribute is not limited to operators syntax, but works just fine for function calls as well. One example that relies heavily on @autoclosure is Swift’s assert function.

var error: NSError? = nil
assert(error == nil, "error is \(error!.localizedDescription)")

The assert call above gets a condition, and a message as arguments. If the message argument in the above example would be always evaluated, the code would crash: We force-unwrap error, which is nil. However, assert uses the autoclosure attribute on the message parameter, and only evaluates the closure if the condition evaluates to false. So we can safely use the force-unwrap in the second parameter, even though error is nil!

On the condition parameter of assert, the autoclosure attribute is also used. That way if we disable assertions, the condition parts stay unevaluated as well, and a compiler optimization can remove them completely.

The beauty of that is although assert has all these features, it can still be implemented as a Swift function! Compare that to C, where assert was implemented as a preprocessor macro, or to Java, where assert had to be added as a keyword. Swift’s autoclosure attribute allows us to write functions and operators that would require compiler changes in a couple of other languages.

Back to our topic: custom operators. As we just implemented the logical conjunction , the logical disjunction is a breeze:

infix operator ∨ { associativity left }
func ∨(lhs: Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs || rhs()
}

The only problem that’s left is that we can get wrong behavior when combining our two infix operators.

assert(true || true && false)  // OK
assert(true ∨ true ∧ false) // FAIL

Adding Precedences

That’s because we haven’t declared precedences yet. Both operators share the same (default) precedence, and both are left associative, so the expression is evaluated as (true ∨ true) ∧ false. But of course we want the operator to have higher precedence, so that we end up with true ∨ (true ∧ false).

In Swift, precedences are declared as numeric values from 0 to 255. Unsurprisingly, a higher value means a higher precedence. The default precedence is 100. As we want our operators and to be alternatives to || and &&, we look up the the precedence values of those two, and use the same values for our operators:

infix operator ∧ { associativity left precedence 120 }
infix operator ∨ { associativity left precedence 110 }

And that’s it. We have added three new operators to the language, using only a couple of lines of code:

prefix operator ¬ {}
prefix func ¬(a: Bool) -> Bool {
return !a
}
infix operator ∧ { associativity left precedence 120 }
func ∧(lhs: Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs && rhs()
}
infix operator ∨ { associativity left precedence 110 }
func ∨(lhs: Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs || rhs()
}

What about operators that change the operands, like assignment operators, or a swap operator? Lets add an operator <-> that swaps two Int values.

infix operator <-> {}
func <->(inout lhs: Int, inout rhs: Int) {
(lhs, rhs) = (rhs, lhs)
}

var a = 42
var b = 23

a <-> b
assert(a == 23 && b == 42)

You may wonder why this works. The function has inout parameters. Shouldn’t we need to use inout expressions, that is prefix the operands with ampersands? We can do that, but we don’t need to: While Swift requires the ampersands for function call syntax, it does not require them if we use operator syntax!

a <-> b        // Totally fine.
a <-> &b // Works as well. Don't do it though.
&a <-> b // Works as well. Don't do it though.
&a <-> &b // Works as well. Don't do it though.

(<->)(&a, &b) // Function call requires & for inout parameters.
(<->)(a, &b) // COMPILER ERROR
(<->)(&a, b) // COMPILER ERROR
(<->)(a, b) // COMPILER ERROR

Overloading Operators in Swift

After we’ve seen how to add operators in Swift, let’s see how we overload existing operators with new functionality. To understand how this works, let’s first have a look at function overloading.

Function Overloading in Swift

In Swift, we can overload a function. That means we give it the same name as another function, but the parameters types, or the return type has to be different. The compiler then tries to infer which function we would like to use.

func foo(i: Int) {
println("foo(i: Int)")
}

func foo(i: String) {
println("foo(i: String)")
}

foo(42) // -> "foo(i: Int)"
foo("The answer") // -> "foo(i: String)"

When the function to be called is ambiguous, we get a compiler error.

func foo() -> Int {
return 42
}

func foo() -> String {
return "The answer"
}

let result = foo() // COMPILER ERROR

In the code snippet above, foo() is ambiguous: Both functions are equally well suited candidates. To resolve the ambiguity, we can help the compiler in different ways. We can select the function with a cast:

let result = (foo as () -> Int)()

That’s not really pretty. A more readable variant is to make the compiler infer the function by casting the result type:

let result = foo() as Int

In my opinion, the best way in this case is to give the receiving variable the correct type:

let result: Int = foo()

Not only is this the shortest variant, but it clearly states what we want: We want to get an Int result from foo().

If you are used to Objective-C, note that the selection of the function is done statically, not dynamically at runtime.

class A {}
class B: A {}

func foo(a: A) {
println("A")
}

func foo(b: B) {
println("B")
}

var a: A = B()
foo(a) // Prints "A", not "B"!

Although the object a is an instance of B at runtime, the selection of the function is made at compile-time based on the type of the variable a.

Operator Overloading in Swift

Just as we can overload functions, we can implement functions for Swift’s operators to achieve operator overloading. We can overload almost any operator. Swift only reserves a couple of operators to prevent changing fundamentals of the language. For instance, we can’t overload the assignment operator =.

Let’s overload the + operator so that it works as a union for NSSet.

func +(lhs: NSSet, rhs: NSSet) -> NSSet {
return lhs.setByAddingObjectsFromSet(rhs)
}

var x = NSSet(objects: "a", "b")
let y = NSSet(objects: "b", "c")
let z = NSSet(objects: "a", "b", "c")
assert(x + y == z)

While we’re at it, we overload the += operator to match our new + operator:

func +=(inout lhs: NSSet, rhs: NSSet) {
lhs = lhs.setByAddingObjectsFromSet(rhs)
}

x += y
assert(x == z)

Note that assignment operators defined in Swift don’t return the result of the assignment, to prevent errors like accidently typing if x = y instead of if x == y for Bool x, y. Our += operator follows that lead.

Examples

Being able to overload most of Swift’s operators is exciting; being able to define own operators even more so! On the other hand, an overloaded operator may be confusing; a new operator may be hard to type, and hard to understand. Let’s have a look at a couple of examples to better understand advantages and disadvantages.

Examples: Operator Overloading

Operators have certain traits we are used to. For example, from math we are used to addition working on values of the same type, being associative, meaning (a + b) + c is always a + (b + c), and being commutative, meaning a + b is always b + a.

In C, the + operator mostly keeps these traits. The only exception I am aware of is pointer arithmetic, where values of different types (a pointer and an integer) are added. Still, the operation stays commutative and associative.

int i = ...;
double *p = ...;

double *p1 = p + i;
double *p2 = i + p;
assert(p1 == p2);

Swift adds another exception: It defines a + operator for concatenating strings and arrays, which is not commutative:

println("Hello" + ", "+ "World")
println([1, 2] + [3, 4])

The Ruby programming language overloads the * operator, allowing to “multiply” an array with an integer, or a string. I think it’s only a matter of time before these appear in a third-party Swift module.

let rects = [CGRectZero]
let result1 = rects * 3

let tags = ["Swift", "Custom", "Operators"]
let result2 = tags * " "

Is that good or bad code? If you don’t know Ruby, try to guess what result1 and result2 are, then check below for the answer.

assert(result1 == [CGRectZero, CGRectZero, CGRectZero])
assert(result2 == "Swift Custom Operators")

A last example of overloading I’d like to show you is from the SwiftAutoLayout library. If you have ever created layout constraints in code, you may have noticed the code looks everything but pretty for simple constraints.

let constraint = NSLayoutConstraint(item: view1, attribute: .Height, relatedBy: .Equal, toItem: view2, attribute: .Height, multiplier: 1.0, constant: 0.0)

With the SwiftAutoLayout library, we can use operators to define constraints.

let constraint = view1.al_height == view2.al_height

It may be confusing at first that the “equal to” operator == returns an NSLayoutConstraint here instead of a Bool. As soon as we get used to that, the code using SwiftAutoLayout is much more readable.

Examples: Custom Operators

When we overload an existing operator, other programmers can guess the meaning of our operator from the original meaning. If we add a custom operator, we can borrow a meaning form another area like math, or logic.

That’s what we did when we added the logical operators. A much more complete version of that is Euler, a library published by Mattt Thompson of NSHipster fame.

We could also borrow a meaning for our custom operator from another programming language. Then at least users of that language would feel right at home. For example, there is a comparison operator <=> in Ruby which we could define in Swift as well. Another example is the swiftgo sample code, an incomplete implementation of goroutines in Swift that borrows Go’s <- operator.

If there is no operator to be found, we can still make one up trying to convey the meaning we want by choosing an adequate symbol. That’s what we did with the swap operator <->.

Recommendations

At the time of writing, Swift is still a young language, so take these recommendations with a grain of salt.

  • We should never redefine predefined Swift behavior. Example: Don’t overload + on [Int] to make it an element-wise addition, as it clashes with the + on arrays which performs concatenation.
  • When overloading an operator, try to keep its general idea, its “spirit” alive. Example: SwiftAutoLayout’s overload of == returns a constraint which specifies two attributes have to be equal to each other. Even though == does not return a Bool, I think it’s OK, because == still stands for a kind of equal to.
  • If you overload an operator for which an assignment variant exists, overload the assignment variant as well if it makes sense. Example: Like shown with + and += for NSSet.
  • If you think about adding an operator, think hard about the tradeoff brevity vs operator understanding. Example: Do ¬, , and carry enough weight as alternatives to !, &&, and ||?
  • If you add an operator, try to borrow a meaning from somewhere else. You can make up a new one as last resort, but then ask yourself why nobody else seemed to have the need for such an operator. Example: The swap operator <->.
  • Most non-ASCII operators are hard to type. If you add non-ASCII operators in your project, make sure to add necessary text replacements via code snippets, or text expansion tools. We want to write code, not copy-paste it.

Simply put in an overused, albeit fitting pop-culture reference: With great power comes great responsibility. Don’t let the list of recommendations discourage you though. Swift is a new language, have fun and experiment!

You’ve reached the end of this article, I hope you learned something new — I certainly learned lots writing it down.

I have one last topic that may not be important enough for all readers, but still may be of interest to some. As before, I put in the last section as a bonus track. Feel free to skip that section, but be back for the next article of this series, which will be about structures!

Bonus Track: Boundaries of Operator Overloading

As Swift allows us to overload operators, I wondered how many of Swift’s operators can actually be overloaded by us: Afew? The majority? Almost all?

We can’t overload a couple of operators because we don’t have the syntax for it: The ternary conditional ? : (we can’t overload ternary operators), the cast operators is, as, and as? (we can’t use alphabetic characters in operators), the optional chaining operator ?, and the nil-coalescing operator ?? (we can’t use a question mark in operators).

In addition, we can’t overload three operators that are reserved by the compiler: the assignment =, the forced unwrapping postfix !, and the & prefix used for in-out parameters.

So we can’t overload 9 operators.

// ternary
? :
// infix
= is as as? ??
// prefix
&
// postfix
? !

But we can overload all remaining 46 operators:

// infix
+ += - -= * *= / /= % %= & &= ^ ^= | |= << <<= >> >>= &+ &- &* &/ &% || && < <= == != === !== >= > ..< ... ~=
// prefix
-- ~ ! + — ++
// postfix
-- ++

If we ignore the documentation and check Swift itself (1.0 in Xcode 6.0.1), we can overload two more operators: There is an undocumented infix operator ~> in the Swift library, and the compiler currently allows us to overload ?? as well.

In summary: Swift allows us to overload all but the most fundamental operators.

That’s all for today. Be back for the next article of this series, which will be about structures!

--

--