“K-O-T-L-I-N, a smooth operator overloadin’ correctly!”

Rage Against The Machine* once covered a musing about smooth operators operating correctly.

Operators operating correctly

Here we are going to cover overloading operators and getting them to operate as correctly and smoothly as possible.

What is operator overloading?

An operator is like a plus sign or a “greater than” sign. You typically think of them in expressions like 1 + 1 or 3 > 2. The operator operates on one or two operands. Most vanilla cases involve using numbers and the language already supports that. Even String is somewhat supported:

val newString = "effec" + "t" // results in "effect"

“Adding” is simple concatenation. But what happens when we subtract?

val newString = "effect" - "e" // what is the meaning of this?!

That won’t compile, in either Java or Kotlin. But let’s say we were writing some program where we had to filter out single characters from text frequently. It might be nice to have the ability to express that function simply as:

val newString = "effect" - "e" // "ffct"

or:

val newString = "team" - "I" // "team" because there is no...

Kotlin associates certain reserved function names like plus and minus to syntactically corresponding (usually intuitive) operators. Here’s how you would overload the minus operator for strings in Kotlin, for the functionality intended above:

operator fun String.minus(filterOut: String): String {
return this.replace(filterOut, "")
}

This is kind of a dirty trick but it works. If we didn’t have ability to use the literal minus sign, we’d have to write:

val newString = "team".minus("I") // ugh, low rent

What we have now is an extension function that works on Strings except it has the keyword operator in front of the signature. We’re forced to implement this as a top-level extension function because we can’t add this operator overload within the String system class itself, but that’s okay.

Overloading operators as class members is preferable. Here’s a somewhat non-obvious example for an overloaded operator:

interface Pet

class Dog : Pet

class Cat : Pet

class Scoop<T : Pet>(val scoops: Int) {
operator fun plus(other: Scoop<T>) = Scoop<T>(this.scoops + other.scoops)
}

val dogFood = Scoop<Dog>(1) + Scoop<Dog>(2)

println("scoops of dog food: ${dogFood.scoops}")

This is will print scoops of dog food: 3.

Image for post
but this only one scoop :(

Above, the operator is overloaded as a member of the class of the objects it acts on. This makes the code cleaner and the logic easier to contain. But all we’ve done so far is just thinly wrap the idea of adding integers- I mean, big whoop, right? Notice a subtle benefit though: we don’t want to mix dog food and cat food together and the generic bound on our plus operator prevents such violations from even compiling!

val grossFood = Scoop<Dog>(1) + Scoop<Cat>(2) //won't compile!

If Scoop wasn’t typed and perhaps instead each object had a val that held an enum type value of either DOG or CAT and we still wanted to enforce not mixing/adding different types of food, we’d have to do something really dumb like check the type field inside the overloaded operator function and throw a runtime exception if someone was trying to add different types. That might’ve flown if you were a Java developer in 2007, but those days are over, kid.

The point of this is not to do a diverted tour of generics, it is to get you thinking about the meaning of overloading an operator.

Sorry I sort of threw in a curveball on generics there. Actually I’m not sorry. I’m going to throw in one more because I can’t help myself. Let’s answer the age old question:

println("Dogs better than cats? ${Dog() > Cat()}")

Right now, this won’t compile. And don’t ever write strings like this where you are initializing objects inline unless you want to get violently thrown out of a job interview. But I digress.

We need to overload the “greater than” sign. In the plus overload we did, the meaning of the function availed itself easily. We’re adding scoops of pet food. Even if we wanted to produce the most complicated addition algorithm ever- like, say, looking at each scoop by the number of individual kibbles- we know that the contract of the addition function is straightforward: take in two Scoop objects, output one Scoop object. The contract doesn’t care how we get there, just that we do.

“Greater than” and the other comparison signs are different. They don’t return objects of the same type- they return true or false. Although you should never compare yourself to others, you’re going to have to now and Kotlin demands you tell it exactly how you stack up against everyone of your peers by implementing the Comparable<T> interface.

interface Pet : Comparable<Pet>

class Dog : Pet {
override fun compareTo(other: Pet): Int = if (other is Cat) 1 else 0
}

class Cat : Pet {
override fun compareTo(other: Pet): Int = if (other is Dog) -1 else 0
}

Here’s our updated class definitions. For the sake of silly examples in a tutorial blog it is sufficient to tell you that compareTo() needs to return a positive number (1) for “this is greater than that”, zero (0) for “this is equal to that”, and a negative number (-1) for “this is less than that.”

So Dog() > Cat() will call the comparison function on the Dog object with Cat as other. Kotlin reads the terms in order from left to right, so if you wanted the Cat’s comparison function to be called with Dog as the other, you’d have to write Cat() > Dog() (which would return false). Any operator that you overload should honor the associative and commutative properties you learned in 6th grade. You should be able to swap and regroup terms and equivalent expressions should evaluate to equivalent values.

We’ve gone ahead and hardcoded the correct universal values for differences between dogs and cats.

println("Dogs better than cats? ${Dog() > Cat()}") //true, of course

It’s silly, but take a nontrivial example of sorting colors into a rainbow. Your Color class will implement Comparable<Color>. You’d probably be doing some math on the RGB values to find the color’s light wavelength, and then do a comparison on the Double values of wavelengths of the two colors. That logic would be taking place inside your compareTo() .

One thing to keep in mind is that while the meaning of numerical operations tends to be narrow, there can often be many meaningful ways to compare objects of the same type. If you choose to implement Comparable<T> be careful what aspect of your objects you choose to compare. It will forever be tied to any expressions you write with < > => =< signs.

(side note: when you write obj1 > obj2 Kotlin calls obj1.compareTo(obj2) under the hood and essentially evaluates something like result > 0 to return true or false for the expression)

A Finer Point

Not all operators take in two things and output another thing, whether it be an object of the same type or true or false. Some operators expect you to alter the one operand object in question. Consider the following:

var a = 1
a += 3
// a == 4

The plusAssign operator, or += can be overloaded. Let’s look at our Scoop class:

class Scoop<T : Pet>(val scoops: Int) {
operator fun plusAssign(other: Scoop<T>): Unit {
this.scoops += other.scoops //UH OH!!
}
}

Oh no! Iceberg straight ahead! Our scoops count is an immutable val . That means that the meaningful information we wish to change can’t be changed in the original Scoop object. With our overloaded plus function we can return that new state into a new Scoop object but here we are borked. Unless we change the scoops member to a var , we’ll never be able to use plusAssign in this way. Objects with immutable state are usually better if you have a choice, so if changing a property to a var would violate design decisions in the rest of your app then you simply don’t need plusAssign.

Overloaded operators are there for us to express things we do to objects in a clearer and concise way. Our motivation should always be to overload operators to get rid of verbose code, repeated code, and boilerplate. We don’t overload all the operators first just to see what kind of interesting things we can make happen (although it is fun). Another place we can overload operators is when we find ourselves mixing objects with function names that sort of hint at an operation. An example of this would be:

val newString = "123".repeat(3) // "123123123"

If Stringplus is already concatenate, why not make times a “repeat”?

operator fun String.times(n: Int) = this.repeat(n)val newString = "123" * 3 // "123123123"

This is nice, but you probably wouldn’t want to do this unless you were making repeated strings a lot in your app. And I mean, a lot. A good way to think about it is if another developer came fresh to your code, their momentary confusion is justified if repeating String concats is something you’ve done all over the place and it’s impossible to miss. It’s probably not justified to overload operators for just little expressions “here and there.”

Also, above, note that we overloaded the meaning of the operator for an operand of a different type. Namely, the integer operand for number of repeats is not a String. There is no restriction on mixing (and overloading) types like this. You just have to make sure that what you’re doing makes sense in the human world.

Make your own operators!

It’s true! You can make your own operators. There is some fine print, though.

infix fun Scoop.spill(percent: Double) = Scoop((this.scoops * percent).roundToInt())

I’ve removed the generic typing of the Scoop class you saw earlier for the sake of this example. Aside from the infix keyword, this is a normal, everyday extension function. When you’re scooping up your pet food for din din time but you accidentally spill some percent of it on the floor, this function tells you how many countable scoops you have left.

val regularScoop = Scoop(scoops = 4)
val sadScoop = regularScoop.spill(percent = 0.5)

That’s using the extension function the usual way. But that infix keyword lets us write this instead:

val sadScoop = regularScoop spill 0.5

That’s “infix notation.” There are rules that come with this. The function name can’t be a reserved operator, it must always operate on two operands, and some other rules. Resist the temptation to use garden variety functions as operators. Let’s say, as a bad example, you had some logic to increment the number of scoops of food because your pet is staring at you and workin’ you hard:

val newScoop = regularScoop petStaringAtMe true

If you made an infix function of that name that took a Boolean then that would be perfectly legitimate code. The problem with that is when you try to understand the “philosophical” meaning when reading it. Consider:

val sadScoop = regularScoop spill 0.5
val newScoop = regularScoop petStaringAtMe true

The first statement almost reads as English. “Take a regular scoop, which is 4 scoops, and spill 50 percent of it.” The second statement, although it makes English sense, isn’t very clear what petStaringAtMe actually does. It almost feels like we’re setting a property and we shouldn’t be using infix notation for that. A fresh developer would have to go read the implementation details to understand that and that strikes me as code smell. Consider:

7 * 9 * 11 * 13

We may not be able to do this in our heads but we know at a very quick glance what’s being done to what and what type of the result it will be and where it will be at the end, because we understand how multiplication works. The operators you overload or use in infix notation should work the same way.

Good grief, that’s not the end?

No. I’m just working up to the great, wondrous, real world example of overloaded operators in Kotlin. And grab your 5th cup of coffee, because I’m going to talk about math.

In hardware sensors that give you information about the movement in the real world the information is often given in structures with 3 values. These values usually correspond to X, Y, and Z dimensions in 3D space. The type of value could be different: acceleration, rate of rotation, magnetic field vectors (a.k.a. fancy compass), etc.. Enter the vector:

data class Vector(val x: Double, val y: Double, val z: Double)

What we’re representing as a data class is a vector: a mathematical object that is basically an arrow that points in a direction and has a magnitude, or it can mean a coordinate in space (technically those are the same thing but I’ll move on ‘cuz y’all didn’t come here for my TED Talk).

When we capture values out of a live, real world stream of information it is best we stuff those values into our immutable Vectors. If you wanted to calculate your absolute orientation in space you will have to combine 3D vectors from several different sensors and then math the shit out of them. I’m talking rotations, scalings, averages, translations, etc. I’m talking reading from foreign hardware so we’re gonna need some bit shifting or byte swapping. I’m talking deep queues of samples for low pass filtering. Woo!

I took some old C++ code for a driver written for an orientation chip known as an MPU9255. This code works, but it’s a little… busy… as C++ can get:

// Now we'll calculate the accleration value into actual g's
ax = MPU9250Data[0]*aRes - accelBias[0]; // get actual g value, this depends on scale being set
ay = MPU9250Data[1]*aRes - accelBias[1];
az = MPU9250Data[2]*aRes - accelBias[2];

// Calculate the gyro value into actual degrees per second
gx = MPU9250Data[4]*gRes; // get actual gyro value, this depends on scale being set
gy = MPU9250Data[5]*gRes;
gz = MPU9250Data[6]*gRes;

MPU9250.readMagData(magCount); // Read the x/y/z adc values

// Calculate the magnetometer values in milliGauss
// Include factory calibration per data sheet and user environmental corrections
mx = magCount[0]*mRes*magCalibration[0] - magBias[0]; // get actual magnetometer value, this depends on scale being set
my = magCount[1]*mRes*magCalibration[1] - magBias[1];
mz = magCount[2]*mRes*magCalibration[2] - magBias[2];
mx *= magScale[0];
my *= magScale[1];
mz *= magScale[2];

What this C++ code is doing is taking values from an accelerometer, gyroscope, and a magnetometer and adjusting the raw read values by some calibration and offset values which have already been calculated. These values next get put into a Madgwick filter, but that story is for another day.

We can see the C++ author is using arrays to store the values for the x, y, and z axes. We also see that how the axes correlate to the element’s position in the array stays consistent. We also see that the operations on each axis value are the same, repeated identically for all 3 dimensions. Hmmm… maybe we could streamline some of this?

We know that the MPU9250Data[] and magCount[] arrays are holding the freshest reads from the chip. These arrays are of Double precision type. With a little shuffling behind the scenes, we can start to corral things in Kotlin:

val accelData: Vector = mpu9255.readAccelData()
val gyroData: Vector = mpu9255.readGyroData()
val magData: Vector = mpu9255.readMagData()

…where mpu9255 is the chip driver, and we’ve done some low-effort refactoring to the driver class to return the triplet values in a Vector from those read functions. Eventually, we need to do some math and put the results in these:

//the outputs
val accel: Vector //accelerometer
val gyro: Vector //gyroscope
val mag: Vector //magnetometer

Looking back at the C++ code, it looks like the author is taking the accelerometer raw data (now in our Vector) and multiplying the components by a single value (a scalar) and then subtracting x, y, z “bias” values. The magnetometer has some additional transformation vectors, so let’s declare:

val accelBias = Vector(..., ..., ...) //Double values for x y z
val magBias = Vector(..., ..., ...)
val magFactoryCalibration = Vector(..., ..., ...)
val magScale = Vector(..., ..., ...)

These are basically constants so the actual values don’t matter for this example.

If we wrote a statement multiplying a Vector by a single number it would currently produce a compiler error. Let’s show Kotlin what we want it to do:

data class Vector(val x: Double, val y: Double, val z: Double) {
operator fun times(other: Double) = Vector(x * other, y * other, z * other)
}

We’re saying to return a new Vector with each of the this's components acted upon individually by the other scalar value. Simple, no? We’re doing nothing different than regular math except we want Kotlin to handle our “grouping” of numbers as one “thing” in our math expression.

Where accelScalar is a single Double value, previously aRes in the C++ code, now this will fly:

accel = accelData * accelScalar  //ex: (2, 4, 6) = (1, 2, 3) * 2

But looking at the original C++ code we see we still have to subtract another “bias” Vector for the accelerometer:

accel = accelData * accelScalar - accelBias //won't compile yet!

We must overload minus but this time with an operand of the same type:

data class Vector(val x: Double, val y: Double, val z: Double) {
operator fun times(other: Double) = Vector(x * other, y * other,
z * other)
operator fun minus(other: Vector) = Vector(x - other.x, y - other.y, z - other.z)
}

Here we act on the components of both this Vector and the other Vector to produce a new Vector. Side note to keep in mind: these overloaded operators are the same as plain old public member functions of this class.

Now we can look at:

accel = accelData * accelScalar - accelBias

… and easily say, in English, “take the latest raw 3D data read, scale it by some factor, and the subtract some 3D bias offset, and store the result in a new Vector (accel) .” When we overload all the mathematical operations for all necessary types we are able to turn this:

// Now we'll calculate the accleration value into actual g's
ax = MPU9250Data[0]*aRes - accelBias[0]; // get actual g value, this depends on scale being set
ay = MPU9250Data[1]*aRes - accelBias[1];
az = MPU9250Data[2]*aRes - accelBias[2];

// Calculate the gyro value into actual degrees per second
gx = MPU9250Data[4]*gRes; // get actual gyro value, this depends on scale being set
gy = MPU9250Data[5]*gRes;
gz = MPU9250Data[6]*gRes;

MPU9250.readMagData(magCount); // Read the x/y/z adc values

// Calculate the magnetometer values in milliGauss
// Include factory calibration per data sheet and user environmental corrections
mx = magCount[0]*mRes*magCalibration[0] - magBias[0]; // get actual magnetometer value, this depends on scale being set
my = magCount[1]*mRes*magCalibration[1] - magBias[1];
mz = magCount[2]*mRes*magCalibration[2] - magBias[2];
mx *= magScale[0];
my *= magScale[1];
mz *= magScale[2];

… into this!

accel = accelData * accelScalar - accelBias
gyro = gyroData * gyroScalar
mag = (magData * magScalar * magFactoryCalibration - magBias) * magScale

It’s so clear you don’t even need the comments! The business logic in each line is entirely literal! We’re expressing how we are transforming our raw data to calibrated data, before we pass it on to the next step in the big fancy algorithm. You can’t even tell what variables are Vector and which are single value Double, and maybe that’s kind of the point! Now there’s zero chance of bugs coming from misplacing an array index number, or a typo of a * getting changed to a + in the cluttered sea of C++ expressions!

Here’s the real Vector class I wrote for this particular application:

data class Vector(val x: Double, val y: Double, val z: Double) {
constructor(list: List<Double>) : this(list[0], list[1], list[2])
constructor(array: DoubleArray) : this(array[0], array[1], array[2])

val list = listOf(x, y, z)
val array = doubleArrayOf(x, y, z)

companion object {
val ZeroVector = Vector(0.0, 0.0, 0.0)
val NonZeroVector = Vector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE)
}

fun toVectorInt() = list.map(Double::roundToInt).toVectorInt()

override fun toString() = "x: $x\t\ty: $y\t\tz: $z"

val quaternion: Quaternion
get() {
// yaw (Z), pitch (Y), roll (X)
// Abbreviations for the various angular functions
val cy = cos(z * 0.5)
val sy = sin(z * 0.5)
val cp = cos(y * 0.5)
val sp = sin(y * 0.5)
val cr = cos(x * 0.5)
val sr = sin(x * 0.5)

return Quaternion(
w = cy * cp * cr + sy * sp * sr,
x = cy * cp * sr - sy * sp * cr,
y = sy * cp * sr + cy * sp * cr,
z = sy * cp * cr - cy * sp * sr)
}

operator fun times(other: Double) = Vector(x * other, y * other, z * other)
operator fun minus(other: Double) = Vector(x - other, y - other, z - other)
operator fun plus(other: Double) = Vector(x + other, y + other, z + other)
operator fun div(other: Double) = Vector(x / other, y / other, z / other)
operator fun rem(other: Double) = Vector(x % other, y % other, z % other)

operator fun times(other: Int) = other.toDouble().let { value -> Vector(x * value, y * value, z * value) }
operator fun minus(other: Int) = other.toDouble().let { value -> Vector(x - value, y - value, z - value) }
operator fun plus(other: Int) = other.toDouble().let { value -> Vector(x + value, y + value, z + value) }
operator fun div(other: Int) = other.toDouble().let { value -> Vector(x / value, y / value, z / value) }
operator fun rem(other: Int) = other.toDouble().let { value -> Vector(x % value, y % value, z % value) }

operator fun times(other: Vector) = Vector(x * other.x, y * other.y, z * other.z)
operator fun minus(other: Vector) = Vector(x - other.x, y - other.y, z - other.z)
operator fun plus(other: Vector) = Vector(x + other.x, y + other.y, z + other.z)
operator fun div(other: Vector) = Vector(x / other.x, y / other.y, z / other.z)
operator fun rem(other: Vector) = Vector(x % other.x, y % other.y, z % other.z)

fun scale(factor: Double) = this * factor //shorthand for times
fun scale(vector: Vector) = this * vector //shorthand for times

fun averageOfComponents() = (x + y + z) / 3
fun unit() = this / sqrt(x * x + y * y + z * z)

operator fun get(i: Int): Double =
when (i) {
0 -> this.x
1 -> this.y
2 -> this.z
else -> throw IndexOutOfBoundsException()
}

fun transform(transform: (Double) -> Int) = VectorInt(transform(x), transform(y), transform(z))
fun transform(transform: (Double) -> Double) = Vector(transform(x), transform(y), transform(z))

fun any(predicate: (Double) -> Boolean) = list.any(predicate)
fun all(predicate: (Double) -> Boolean) = list.all(predicate)
}

There’s obviously more here but one thing we didn’t consider in this tutorial example is when you want to multiply a Vector by an Int scalar instead of a Double. You do have to manually add those overloaded operators with operands of type Int, but it is no different than what we did with the Double one. It can be a bit boilerplate-y but overloading operators can be very worth it!

Image for post
an MPU-9250 reading the orientation vector of this snout

*yes, I’m aware it is a cover, thank you

Written by

dog farmer, coder

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store