12/08/2018, 17:03

Custom operators in Swift

Few Swift features cause as much heated debate as the use of custom operators. While some people find them really useful in order to reduce code verbosity, or to implement lightweight syntax extensions, others think that they should be avoided completely. Love ’em or hate ’em — either ...

Few Swift features cause as much heated debate as the use of custom operators. While some people find them really useful in order to reduce code verbosity, or to implement lightweight syntax extensions, others think that they should be avoided completely. Love ’em or hate ’em — either way there are some really interesting things that we can do with custom operators — whether we are overloading existing ones or defining our own. This week, let’s take a look at a few situations that custom operators could be used in, and some of the pros & cons of using them.

Sometimes we define value types that are essentially just containers for other, more primitive, values. For example, in a strategy game I’m working on, the player can gather two kinds of resources — wood & gold. To model these resources in code, I use a Resources struct that acts as a container for a pair of wood & gold values, like this:

struct Resources {
    var gold: Int
    var wood: Int
}

Whenever I’m referring to a set of resources, I’m then using this struct — for instance to keep track of a player’s currently available resources:

struct Player {
    var resources: Resources
}

One thing you can spend your resources on in the game is to train new units for your army. When such an action is performed, I simply subtract the gold & wood cost for that unit from the current player’s resources:

func trainUnit(ofKind kind: Unit.Kind) {
    let unit = Unit(kind: kind)
    board.add(unit)
    currentPlayer.resources.gold -= kind.cost.gold
    currentPlayer.resources.wood -= kind.cost.wood
}

Doing the above totally works, but since there are many actions in the game that affects a player’s resources, there are many places in the codebase where the two subtractions for gold & wood have to be duplicated.

Not only does that make it easy to miss subtracting one of these values, but it makes it much harder to introduce a new resource type (say, silver), since I’d have to go through the entire code base and update all the places where resources are dealt with.

Let’s try using operator overloading to solve the above problem. When working with operators in most languages (Swift included), you have two options. Either, you overload an existing operator, or you create a new one. An overload works just like a method overload, in that you create a new version of an operator with either new input or output. In this case, we’ll define an overload of the -= operator, that works on two Resources values, like this:

extension Resources {
    static func -=(lhs: inout Resources, rhs: Resources) {
        lhs.gold -= rhs.gold
        lhs.wood -= rhs.wood
    }
}

Just like when conforming to Equatable, operator overloads in Swift are just normal static functions that can be declared on a type. In the case of -=, the left hand side of the operator is an inout parameter, which is the value that we are mutating.

With our operator overload in place, we can now simply call -= directly on the current player's resources, just like we would on any primitive numeric value:

currentPlayer.resources -= kind.cost

Not only does that read pretty nicely, it also helps us eliminate our code duplication problem. Since we always want all outside logic to mutate Resources instances as a whole, we can go ahead and make the gold and wood properties readonly for all other types:

struct Resources {
    private(set) var gold: Int
    private(set) var wood: Int
    
    init(gold: Int, wood: Int) {
        self.gold = gold
        self.wood = wood
    }
}

The above works thanks to a change in Swift 4, which gave extensions defined in the same file private privileges. So our -= operator overload (and any other operators or APIs that we define for Resources) can mutate properties without needing them to be publicly mutable.

Another way we could’ve solved the Resouces problem above would be to use a mutating function instead of an operator overload. We could've added a function that reduces a Resources value's properties by another instance, like this:

extension Resources {
    mutating func reduce(by resources: Resources) {
        gold -= resources.gold
        wood -= resources.wood
    }
}

Both solutions have their merits, and you could argue that the mutating function approach is more explicit. However, you also wouldn’t want the standard subtraction API for numbers to be something like 5.reduce(by: 3), so perhaps this is a case where overloading an operator makes perfect sense.

Let’s take a look at another scenario in which using operator overloading can be quite nice. Even though we have Auto Layout and its powerful layout anchors API, sometimes we find ourselves in situations when we need to do manual layout calculations.

In situations like these, it’s very common to have to do math on two dimensional values — like CGPoint, CGSize and CGVector. For example, we might need to calculate the origin of a label by using the size of an image view and some additional margin, like this:

label.frame.origin = CGPoint(
    x: imageView.bounds.awidth + 10,
    y: imageView.bounds.height + 20
)

Instead of having to always expand points and sizes to use their underlying components, wouldn’t it be nice if we could simply add them up (just like we did with our Resources struct)?

0