Custom Operators in Swift

 ยท 6 min
Brett Sayles on Pexels

Swift is quite a flexible language, providing you with many tools to modify and augment it as you seem fit. One of these augmentations is the support for custom and overloaded operators.


What Are Custom Operators?

Swift allows you to define your own operators besides the predefined set of operators, like ==, +, -, etc. There are three different types: prefix, postfix, and infix operators.

Prefix and Postfix

As their names suggest, these operators either precede or succeed their respective target, like the increment/decrement operators ++/--. They are, therefore, “unary” operators, affecting a single value.

To declare a prefix or postfix operator, we need to create a static function with the correct modifier – prefix or postifx – in an extension of the target type:

swift
prefix operator !!

extension String {

  static prefix !! (value: String) -> String {
    value.uppercased()
  }
}

let greeting = "Hello, World!"
print(!!greeting) // HELLO, WORLD!

First, we need to define the operator. The possible characters for operators are limited to not interfere with identifiers for other things like types, functions, etc. The complete list is available in “The Swift Programming Language”.

Putting the operator logic into an extension isn’t actually required. A globally available func would also be ok. But operators in an extension for their respective type is a neat way to group them logically.

Infix

As a “binary” operator, the third kind works between its targets, and has access to both, like the arithmetic operators +, -, *, /, etc.

They are defined very similarly to infix_/prefix operators:

swift
infix operator -

extension String {

  // <String> - <Int> = Remove characters from the end
  static func -(lhs: String,
                rhs: Int) -> String {

    guard rhs > 0 else {
      return lhs
    }

    guard let idx = lhs.index(lhs.endIndedx,
                              offsetBy: -(rhs+1),
                              limitedBy: lhs.startIndex) else {
      return ""
    }

    return String(lhs[...idx])
  }
}

let greeting = "Hello, World!"

print(greeting - 8) // Hello

An already existing infix operator - doesn’t have to be redefined, but the compiler isn’t complaining either. The related types don’t have to be identical, just as we can return whatever type we please. That allows for quite flexible operators, like creating intermediate types to create a builder-like domain-specific language.


Declaring the Operator

To define the function of an operator, itself has to be defined first. If we don’t want to override or reuse an existing modifier, we must define its precedence and associativity ourselves.

The order of operations, also known as “operator precedence”, defines the rules between different operators in expressions. For example, the expression 2 + 3 * 4 will result in 14 because the expression is equivalent to 2 + (3 * 4) due to the operator precedence.

Associativity defines how operators of the same precedence should behave if used together. Depending on our logic, there might be a difference in the expression a !! b !! c when it’s evaluated (a !! b) !! c compared to a !! (b !! c). That’s why we can define how, and even if, multiple operators can be chained together.

There are multiple precede groups already defined, and if our operator doesn’t use an existing one, it defaults to the DefaultPrecedenceGroup.

The groups are sorted “highest-to-lowest”.

GroupAssociativityExample operators
BitwiseShiftPrecedencenone<< >>
MultiplicationPrecedenceleft* /
AdditionPrecedenceleft+ -
RangeFormationPrecedencenone... ..<
CastingPrecedencenoneis as as? as!
NilCoalescingPrecedenceright??
ComparisonPrecedencenone!= == < > <= >=
LogicalConjunctionPrecedenceleft&&
LogicalDisjunctionPrecedenceleft||
DefaultPrecedencenone
TernaryPrecedenceright? :
AssignmentPrecedenceright= += -=

A custom precedence group allows us to position our operators in between already existing ones:

swift
precedencegroup <name of the group> {

  lowerThan: <other group>

  higherThan: <other group>

  associativity: <left/right/none>

  assignment: <true/false>
}

Precedence position and associativity were already mentioned in the article. But what is the assignment property?

As written in the Swift Proposal SE-0077, assignment: true will fold the operator into an optional chain, allowing foo?.bar += 2 to work as foo?(.bar += 2) instead of failing to type-check as (foo?.bar) += 2.

Getting these operator properties right might need some “trial-and-error”, or even better, a set of unit tests to define the exact behavior, especially if they are part of a shared framework.


Overloading Operators

Another pitfall to be aware of is overloading existing operators. Nothing is stopping you from redefining such a fundamental operator like + for Int:

swift
extension Int {

  static func +(lhs: Int,
                rhs: Int) -> Int {

    return lhs * rhs;
  }
}

I don’t think there’s an explanation needed why this is a terrible idea. If you’re overloading existing operators or reusing the same operator with different types, make sure that its purpose is clear.


The Downsides of Custom Operators

Even though custom operators are easy to create, and the possible use cases for custom operators are manifold, I strongly discourage you from (over-)using them!

As with extensions, we introduce technical debt into our projects in the form of a new, non-standard operator. Even worse, it might be a familiar operator that’s reused in a new context. Onboarding developers might not grasp what is happening and need to search for documentation or the operator declaration itself. That will require a certain amount of mental capacity to understand the “mini-language” on top of Swift itself. And you can’t CMD-click on the operator to find its definition, like you can do it with an extension method.

For example, I’ve created a library to simplify creating AutoLayout code with extensions and custom operators:

swift
// BEFORE
let constraint = left.rightAnchor.constraint(equalsTo: right.leftAnchor,
                                             constant: 8.0)
constraint.priority = .required
constraint.identifier = "Left-To-Right"

// WITH EXTENSIONS
let constraint = view.topAnchor.equalTo(other.topAnchor, 8.0)
                               .priority(.required)
                               .identifier("Left-To-Right")

// WITH CUSTOM OPERATORS
let constraint = left.rightAnchor |==| right.leftAnchor + 8.0
                                   ~~ .required
                                   ~~ "Left-To-Right"

At first, I created a few extensions to save me from typing constraint( or constant: over and over again. The code is more readable and concise, and no explanation is needed of what’s happening. All the required information is still there.

I’ve also created custom operators for this article to create a “domain-specific language”-approach that’s more visual than the extension methods. Regardless of possible immediate productivity improvements while working on the code, it might and will likely become a problem in the long run.

Apple isn’t trying really hard to not break Xcode, Swift, or the iOS SDK with every release… Even without custom operators or extensions, our code might break unexpectedly. But by introducing such augmentations, we’re increasing the possible surface for breakage. So besides our “normal” code, we also have to maintain the “supporting” code for the project’s lifespan. And reverting to “un-augmented” code might not be as simple as a “search & replace”.


Conclusion

As with every other tool and dependency at your disposal, you need to know when it’s worth including it and realize the long-term implications on your project. That doesn’t mean you should never use custom operators. They’re a great tool to simplify expressions. But don’t paint yourself into a corner for short-term gain.

Extension methods are a good alternative, providing a more verbose surface and discoverability to interpret their use case, even if someone isn’t familiar with what’s happening “behind the scenes”. But they won’t provide you with such control over their use, like setting specific precedence or associativity.


Resources