Thursday 3 November 2022

Kotlin: more operator overloading

G'day:

The Kotlin koans are still focusing on operator overloading, so so am I. Previously (Kotlin: overriding operators), I had a mess around with overloading:

This evening I'm gonna look at a coupla others.

Plus operator

Like… just the + operator. As in a + b.

This is a daft example, but I'm gonna define a buildable Critter class. A Critter can have a number of arms, legs and heads. Why? Oh I don't know. It just seemed funny to me at the time.

To do this I need to overload the plus operator multiple times:

class Critter(){
    private var heads = mutableListOf<Head>()
    private var arms = mutableListOf<Arm>()
    private var legs = mutableListOf<Leg>()

    operator fun plus(head: Head) : Critter {
        heads.add(head)
        return this
    }
    operator fun plus(arm: Arm) : Critter {
        arms.add(arm)
        return this
    }
    operator fun plus(leg: Leg) : Critter  {
        legs.add(leg)
        return this
    }
}

I'm overloading the plus operator three times. One each for Critter + Head, Critter + Arm, Critter + Leg. Note also that I'm specifically returning the Critter itself from these operations, so that further operations can be chained.

For the purposes of this exercise, the Arm, Leg, Head classes are a bit light on implementation:

class Arm
class Leg
class Head

For the test, I've also added a helper method:

fun describe()= """
        Critter with ${heads.size} ${English.plural("head", heads.size)},
        and ${arms.size} ${English.plural("arm", arms.size)},
        and ${legs.size} ${English.plural("leg", legs.size)}
    """.trimIndent().replace("\n", " ")

(That pluralising thing is a third-party library I dug up. No need to worry about that just now).

And now we can have the rest of our test code (all the above was the "arrange" part of the test):

val critter = Critter()
critter + Head()
critter + Arm() + Arm()
critter + Leg() + Leg() + Leg() + Leg()

critter.describe() shouldBe "Critter with 1 head, and 2 arms, and 4 legs"

It's silly, but I quite like that.

For the sake of completeness, I've tested the chaining works with different types:

fun `it can mix-up the body parts` () {
    val critter = Critter() + Head() + Arm() + Leg() + Leg() + Arm() + Leg() + Head() + Leg()

    critter.describe() shouldBe "Critter with 2 heads, and 2 arms, and 4 legs"
}

Invoke operator

I've messed around with this sort of thing with PHP's __invoke before. The invoke operator function represents the () operator one calls functions with; IE: if f is the function, one uses the () operator to call it: f(). Implementing this operator on a class lets one call objects like methods. Should one want to do that. Which, admittedly, is pretty rarely. But still.

class Logger {
    private var _log = mutableListOf<String>()
    val log: List<String>
        get() = _log

    operator fun invoke(message: String) {
        _log += message
    }
}

Here I have a logger class whose objects can be called as methods:

val logMessage = Logger() // logMessage is an object: an instance of Logger
logMessage("This is a log message")
logMessage("This is another log message")

The bit that effects this is the invoke function.

And the assert part of the test:

logMessage.log shouldBe listOf("This is a log message", "This is another log message")

The next thing I thought to try is how I might have a variadic invoke function: one I can pass any number of arguments to. This example is a bit contrived, but it shows it. Here a Task is an object that is initialised with a lambda, and when the task is invoked as a function, it runs the lambda with whatever the invocation is passed:

class Task(var lambda : (args: Array<String>) -> List<String>) {
    operator fun invoke(vararg args: String) : List<String> {
        return lambda(arrayOf(*args))
    }
}

One thing that is annoying is that a lambda can't be defined as having a vararg parameter, so I need to use an array of things there. However the invoke function can use vararg, so that's cool.

The * there is the spread operator, which converts the varargs passed to the invoke call to an array that the lambda needs. I could not find any other docs for this other than a reference in the vararg docs linked in the previous paragraph.

So I create my task with its lambda:

val task = Task { args -> args.toList() }

And I check the results:

val taskResult = task("This", "is", "a", "variadic", "invoke", "handler")
taskResult shouldBe listOf("This", "is", "a", "variadic", "invoke", "handler")

Both those lines look similar, so it's important to note the first one is passing six separate arguments to task (which is an object remember, not a function!), and the second line is one list with six items. The lambda coverted the varargs from the invoke call to the list it returned.


And that is as far as I got tonight, so I'll leave off here.

Here's the code:

Righto.

--
Adam