Sunday, 30 October 2022

Kotlin: overriding operators

G'day:

Apologies in advance if this article is a bit rubbish. I'm not really "feeling it" today with the writing, but I need to crack on with the Kotlin stuff so am hoping if I get a move on with something, I'll find my motivation as I go. This has never once worked in my life before, but let's see what happens this time.


Prelude

OK so the next Kotlin koans exercise is about operator overloading, hence looking at that now. My one enduring memory of when I did C++ back in polytech (this was in 1993 or 1994) was my tutor's advice re operator overloading: just because it's possible doesn't mean one should do it: it just makes for messy, hard to understand code. 28 years later (OMG: 28!!) I am using a language that supports operator overloading for the first time. I mean I've messed around with Ruby, Groovy etc which allow it, but I never got far enough with those to need to look at it.

I think this is possibly why I'm unmotivated to write on this: I need to know about it, but I don't really want to know about it, if you see what I mean. Anyway: harden up, Cameron. Get on with it.


Unary plus operator

The first interesting thing I landed on was the notion of a unary-plus operator. WTF? I knew what a unary-minus operator does: switches the sign on the operand. But what would a unary-plus operator do? The operator overloading docs page mentioned it but didn't explain it; and the operators listing in the docs (Keywords and operators › Operators and special symbols) didn't mention it at all (or unary-minus for that matter). I've just found there's a docs page for unary-plus specifically, and it says:

operator fun unaryPlus(): Int

Returns this value.

That was a bit unhelpful until I checked the equivlent page for unary-minus:

operator fun unaryMinus(): Int

Returns the negative of this value.

OK: got it. It literally just returns the value (constrast with the minus one that returns the negative of the value, to slightly belabour the point). This does not seem useful, so I figured I was missing something, so googled a bit. I found a doc somewhere (I can't find it now; I'm writing this a week after I did the initial research) that indicated it was added to either C or C++ for the sake of symmetry with unary-minus. I also found that in other languages it does actually have functionality on non-numeric types. In JavaScript it will return the numeric value of a string, eg:

s = "42"
'42'
+s 
42  

And in Java I found that it does similar. What is the purpose of Java's unary plus operator?:

The unary plus operator performs an automatic conversion to int when the type of its operand is byte, char, or short.

Let's have a look at that:

jshell> char c = 'A'
c ==> 'A'
|  created variable c : char

jshell> +c
$2 ==> 65
|  created scratch variable $2 : int

Fair enough. I can work with this.

I had a look at how the unary-plus operator works on the types that support it in Kotlin, with variations on this approach:

@Nested
@DisplayName("Tests of unary plus operator on Ints")
inner class IntUnaryPlusTest {

    @Test
    fun `it does nothing to integer values`() {
        +1 shouldBe 1
    }

    @Test
    fun `it throws a NPE for null Ints`() {
        val i: Int? = null

        shouldThrow<NullPointerException> {
            +i!!
        }
    }
}

I had a version of those for Ints, Doubles, Floats, Longs, Shorts and Bytes.


Operator overloading

Kotlin doesn't support the unary-plus operator on chars, so here's where my experimentation starts. It's pretty simple:

@Nested
@DisplayName("Tests of overloaded unary plus operator on Chars")
inner class CharUnaryPlusTest {
    operator fun Char?.unaryPlus(): Int? {
        return this?.code
    }

    @Test
    fun `it returns the character code of the char as an int`() {
        +'a' shouldBe 97
    }

    @Test
    fun `it returns null if the Char is null`() {
        val c: Char? = null

        val result = +c

        result.shouldBeNull()
    }

    @Test
    fun `it handles double-byte chars`() {
        +'€' shouldBe 8364
    }

    // NB: emoji will not fit into a Char see https://stackoverflow.com/q/70152643/894061
}

All one does is to create a method of your class or - in this case as I am "extending" a built-in class - an extension function, prefix it with "operator", and the method names for the various operators are listed on the docs page I link to in the heading. That's it.

I also did an implementation for Booleans:

operator fun Boolean?.unaryPlus(): Int {
    return if (this == true) 1 else 0
}

// ...

@Test
fun `it returns zero if the boolean is null`() {
    val b: Boolean? = null

    val result = +b

    result shouldBe 0
}

(I left the null test in as it wasn't so obvious: as an arbitrary decision I decided a null Boolean should behave as if it's falsey for this conversion. Equally it could have just stayed as a null).

I also did Strings:

@Nested
@DisplayName("Tests of overloaded unary plus operator on Strings")
inner class StringUnaryPlusTest {
    operator fun String?.unaryPlus(): Number? {
        return this?.toIntOrNull() ?: this?.toDoubleOrNull()
    }

    @Test
    fun `it converts a string integer to an Int`() {
        val result = +"42"

        result.shouldBeInstanceOf<Int>()
        result shouldBe 42
    }

    @Test
    fun `it handles negative numbers`() {
        +"-42" shouldBe -42
    }

    @Test
    fun `it converts a string double to a Double`() {
        val result = +"3.14"

        result.shouldBeInstanceOf<Double>()
        result shouldBe 3.14
    }

    @Test
    fun `it handles a null string`() {
        val s: String? = null

        val result = +s

        result.shouldBeNull()
    }

    @Test
    fun `it handles a string that is not a number`() {
        val s = "not a number"

        val result = +s

        result.shouldBeNull()
    }
}

Again the handling was an arbitrary one. I decided that if it could convert it to an Int, then an Int it will be. Otherwise a Double. Otherwise null.

I also let Copilot take over after the obvious cases. I typed the test label and let it do the rest. It wrote some more examples for collection types along these lines:

@Test
fun `unary plus is not implemented on Array values, but can be overloaded`() {
    operator fun Array<Int>.unaryPlus(): Int? {
        return this.sum()
    }
    +arrayOf(1, 2, 3) shouldBe 6
}

Indeed after the first one, I just had to write @Test and it got the idea. I won't repeat it all here as it's a bit "samey", but I'll include a link to GitHub at the bottom of the article.


Koans requirement: compareTo

That was a lot of digression away from what the exercise actually wanted, which is to implement date-comparison operations on bespoke date class. The example code is this:

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable<MyDate> {
    /* TODO */
}

And I need to be able to do this:

println(date1 < date2)

All this investigation into overloading unary operators has not helped here. I need to overload an operator that has two operands.

Copilot largely came to the rescue here. I created the test file and @DisplayName("Tests of overloading the compareTo operator on MyDate objects") and it created the test skeletons for me:

@Test
fun `it should returns 0 when the two MyDate objects are equal`() {
    TODO()
}

@Test
fun `it should returns -1 when the first MyDate object is less than the second MyDate object`() {
    TODO()
}

@Test
fun `it should returns 1 when the first MyDate object is greater than the second MyDate object`() {
    TODO()
}

It couldn't do better than that as it had no idea what this "MyDate" thing is that I'm on about. However once I typed data class MyDate, it got the idea and had a good stab at the solution:

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable<MyDate> {
    override fun compareTo(other: MyDate): Int {
        return when {
            year != other.year -> year - other.year
            month != other.month -> month - other.month
            else -> dayOfMonth - other.dayOfMonth
        }
    }
}

That would work OK, but it failed its own tests because it returned 5 for the comparison in the "less than" test (2016 - 2011 = 5). I coached it a bit, and we finally agreed on this:

override fun compareTo(other: MyDate): Int {
    return when {
        year != other.year -> year.compareTo(other.year)
        month != other.month -> month.compareTo(other.month)
        else -> dayOfMonth.compareTo(other.dayOfMonth)
    }
}

This way it returns one of -1, 0, 1, which is tidier than the actual difference, and has the added bonus of giving us passing tests.

I need one more test though. The requirement is not to us the compareTo method, it's to use the < operator. I will encourage Copilot to write this for me:

(It's only got the red squiggly line cos the function name isn't closed yet).

Nailed it, Copilot. Thanks.

I slung that into the koans window and checked it: pass. I checked their own implementation, and it was slightly simpler:

override fun compareTo(other: MyDate) = when {
    year != other.year -> year - other.year
    month != other.month -> month - other.month
    else -> dayOfMonth - other.dayOfMonth
}

All that when block is a single expression, so we could have eschewed the braces and the return. Good thinking.


Indexed access operator

I decided to do one last exercise. One can make an object accessible via index notation (eg: myObj[i]) via overloading the get operator.

Here's how we expect it to work:

import WordCollections.MaoriNumbersEnum as MI

// ...

@Test
fun `it should return the correct value for the given index`() {
    val numbers = MaoriNumbers(listOf(MI.TAHI,MI.RUA,MI.TORU,MI.WHA))

    numbers[0] shouldBe MI.TAHI
    numbers[1] shouldBe MI.RUA
    numbers[2] shouldBe MI.TORU
    numbers[3] shouldBe MI.WHA
}

And here's the implementation:

data class MaoriNumbers(val numbers: List<MI>) {
    operator fun get(index: Int): MI {
        return numbers[index]
    }
}

Easy. And this is actually a good use of operator overloading I think.

Also note I am using an enum here to restrict the values that my MaoriNumbers object will support:

enum class MaoriNumbersEnum {
    TAHI, RUA, TORU, WHA, RIMA, ONO, WHITU, WARU, IWA, TEKAU
}

This is a call-back to my earlier article "Kotlin: more koans, more random investigation".


OK: that's enough. I seem to've got into the swing of things with the writing after about 15min. This is by no means my best article ever, but I think it's readable, and in writing everything thing down it's solidified things in my head a bit better. Cool. Do give me a rark-up if this reads like I'm just going through the motions a bit, it'd be good to know what other ppl think.

Oh yeah! The code! It's here: /src/test/kotlin/junit/language/operators.

Righto.

--
Adam