Showing posts with label Kotlin. Show all posts
Showing posts with label Kotlin. Show all posts

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

Monday 31 October 2022

Kotlin: ranges

G'day:

Yet another example of me starting a Kotlin koans exercise, seeing the first interesting word in the koans task description and getting lost in researching how the thing works.

Test class names

This is just an aside. You know how I've been showing all these test methods like this (spoiler for the next section):

@Test
fun `a range of integers should be an IntRange`() {
    val range = 1..10

    range.shouldBeInstanceOf<IntRange>()
}

Where the method name is in back-ticks and a human-readable sentence? On a whim I went "I wonder if…" and I tried this:

internal class `Tests of ranges` {

    @Nested
    inner class `Tests of ranges on Ints` {
    // etc

And it only bloody works on class names too! How cool is that?

In the test output that lot shows like this:


Ranges

From the docs:

A range defines a closed interval in the mathematical sense: it is defined by its two endpoint values which are both included in the range.

Let's look at some tests that show how they work:

fun `it should return true for a value in a range`() {
    val range = 1..10
    val value = 5

    (value in range) shouldBe true
}
fun `it should return false for a value not in a range`() {
    val range = 1..10
    val value = 11

    (value in range) shouldBe false
}
fun `it should return true for values at the boundaries of the range`() {
    val range = 1..10

    (1 in range) shouldBe true
    (10 in range) shouldBe true
}

Those are pretty straight forward. What type is it?

fun `a range of integers should be an IntRange`() {
    val range = 1..10

    range.shouldBeInstanceOf<IntRange>()
}

And how to use it?

fun `it can be iterated over`() {
    val range = 1..10
    var list = mutableListOf<Int>()

    list = range.fold(list) { acc, i -> acc.apply { add(i) } }

    list shouldBe listOf(1,2,3,4,5,6,7,8,9,10)
}

It can have a step:

fun `it should return true for values at the steps`() {
    val range = 1..10 step 2

    listOf(1,3,5,7,9).forEach {
        (it in range) shouldBe true
    }
}

Also be mindful of what elements are in the range:

@Test
fun `the upper bound might not be included`() {
    val range = 1 until 10 step 2

    (10 in range) shouldBe false
}
fun `it should return false for values not at the steps`() {
    val range = 1..10 step 2

    listOf(2,4,6,8,10).forEach {
        (it in range) shouldBe false
    }
}

Note the type on a stepped-range is different:

fun `a stepped range is an IntProgression`() {
    val range = 1..10 step 2

    range.shouldNotBeInstanceOf<IntRange>()
    range.shouldBeInstanceOf<IntProgression>()
}

They work in reverse:

fun `a step can be downwards`() {
    val range = 10 downTo 1 step 2

    listOf(10,8,6,4,2).forEach {
        (it in range) shouldBe true
    }
    (1 in range) shouldBe false
}

And one can access some properties about the range:

fun `it has some properties`() {
    val range = 1..10 step 2

    range.first shouldBe 1
    range.last shouldBe 9
    range.step shouldBe 2
}

Note how it's the max value here, not the upper bound of the range.

And they can be reversed (and by inference here, compared):

fun `it can be reversed`() {
    val range = 1..10 step 2
    val reversed = range.reversed()

    (reversed == (9 downTo 1 step 2)) shouldBe true
}

One can make a range of doubles, but they're not iterable:

fun `it does not implement iterator like a range of ints does`() {
    val intRange = 1..10
    intRange.shouldBeInstanceOf<Iterable<*>>()

    val doubleRange = 1.0..10.0
    doubleRange.shouldNotBeInstanceOf<Iterable<*>>()
}

However one can still check whether something is within the range:

fun `its elements can be compared to other values`() {
    val range = 1.0..10.0
    val value = 5.5

    (value in range) shouldBe true
}

Ranges of chars are more useful (I guess…):

fun `a range of chars can have a step`() {
    val range = 'z' downTo 'a' step 2

    listOf('z','x','v','t','r','p','n','l','j','h','f','d','b').forEach {
        (it in range) shouldBe true
    }
}

One can have ranges of strings, but they don't seem to support stepping or downto

fun `a range of strings can have a step`() {
    val range = "a".."f"

    listOf("a","b","c","d","e","f").forEach {
        (it in range) shouldBe true
    }
}

But they can be multi-char:

fun `the strings in the range can be multi-character`() {
    val range = "aa".."cc"

    listOf("aa","ab","ac","ba","bb","bc","ca","cb","cc").forEach {
        (it in range) shouldBe true
    }
}

I tried to be clever and create a range of strings and then reverse it, but Kotlin sez no:

I can make a range of enum elements (which I thought was at least a bit clever of me to try):

@Test
fun `a range of MaoriNumbersEnum should not allow other elements from the enum`() {
    val range = MI.TAHI..MI.WHA // tahi, rua, toru, wha

    (MI.RUA in range) shouldBe true
    (MI.RIMA in range) shouldBe false // rima is 5
}

@Test
fun `a range of enums is not iterable` () {
    val range = MI.TAHI..MI.WHA

    range.shouldNotBeInstanceOf<Iterable<*>>()
}

But as you can see, they too aren't iterable.

I think I need to set myself an assignment to write extension functions so I can iterate over my Maori numbers. But not this evening.

Lastly - and so I can answer the koans question - any class that implements Comparable can be used in a range.

Here's my MyDate class back from the previous article:

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.compareTo(other.year)
            month != other.month -> month.compareTo(other.month)
            else -> dayOfMonth.compareTo(other.dayOfMonth)
        }
    }
}

And using it in a range:

fun `a range of objects implementing Comparable can be used as a range`() {
    val testDates = MyDate(1955, 3, 25)..MyDate(1955, 3, 28)

    testDates.start shouldBe MyDate(1955, 3, 25)
    testDates.endInclusive shouldBe MyDate(1955, 3, 28)

    val restDay = MyDate(1955, 3, 27)
    (restDay in testDates) shouldBe true
}

(26 bonus points for anyone who can work out what that date range is. There is a clue in there. And I doubt anyone who is not an NZ cricket fan has a hope in hell of getting it).

It looks like making an iterable range of MyDate objects isn't that hard (esp if I get Copilot to do a lot of it). First I need to implement a "next" method in MyDate:

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable<MyDate> {
    private val date = LocalDate.of(year, month, dayOfMonth)

    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)
        }
    }

    fun nextDay(): MyDate {
        val nextDate = date.plusDays(1)
        return MyDate(nextDate.year, nextDate.monthValue, nextDate.dayOfMonth)
    }
}

Then I need to create the range class:

class MyDateRange(override val start: MyDate, override val endInclusive: MyDate) : ClosedRange<MyDate>, Iterable<MyDate> {
    override fun iterator(): Iterator<MyDate> {
        return object : Iterator<MyDate> {
            var current = start

            override fun hasNext(): Boolean {
                return current <= endInclusive
            }

            override fun next(): MyDate {
                val result = current
                current = current.nextDay()
                return result
            }
        }
    }
}

Copilot wrote all of that for me, once I gave it the class name, and then asked it to implement Iterable as well.

I have to admit I kinda spiked this code to see how hard it would be to implement before deciding whether to even include this in the article, so I back-filled the test on this one.

fun `an iterable range can be made by implementing Comparable and Iterable`() {
    val testDates = MyDateRange(MyDate(1955, 3, 25), MyDate(1955, 3, 28))

    (MyDate(1955, 3, 27) in testDates) shouldBe true
    (MyDate(1955, 3, 29) in testDates) shouldBe false

    var list = mutableListOf<MyDate>()

    list = testDates.fold(list) { acc, date -> acc.apply { add(date) } }

    list shouldBe listOf(
        MyDate(1955, 3, 25),
        MyDate(1955, 3, 26),
        MyDate(1955, 3, 27),
        MyDate(1955, 3, 28)
    )
}

This is a good start for me to work out how to make a fully-functioning enum range. Later.

And that is a chunk of code, and not much text. I think the tests largely speak for themselves; the only tricky bit was that last MyDateRange thing, and I'll come back to that. Lemme know if this strategy of using "tests as narrative" works? Cheers.

The code is here: RangesTest.kt.

Righto.

--
Adam

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

Saturday 22 October 2022

Kotlin: more koans, more random investigation

G'day:

I started doing the Kotlin koans a while back, and discuss my meagre progress in these articles:

I used the koans as a jumping off point to look at stuff. My mindset was "OK, I need to do [whatever] to make the koans exercise pass, but what is that all about?" And today I shall continue.


TODO

In Kotlin a todo is not just a marker of guilt and a pretence one will come back and finish things later, but an actual function:

@DisplayName("Tests of the TODO function")
class TodoTest {
    @Test
    fun `It should throw a NotImplementedError`() {
        shouldThrow<NotImplementedError> {
            TODO()
        }
    }

    @Test
    fun `It should throw a NotImplementedError with a message`() {
        shouldThrow<NotImplementedError> {
            TODO("This is a message")
        }.message shouldBe "An operation is not implemented: This is a message"
    }

}

I guess this is a better approach than to leave a comment to that effect in the code. Harder to just ignore.


enum classes

Enums are summarised well on Wikipedia ("Enumerated type"):

In computer programming, an enumerated type […] is a data type consisting of a set of named values […]. The enumerator names are usually identifiers that behave as constants in the language.

For these tests I'm using some enums I've abstracted out into a separate reference class as I use these Maori terms all the time, so I'm finally getting around to putting them in one place.

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

enum class MaoriDaysEnum {
    RĀHINA, RĀTŪ, RĀAPA, RĀPARE, RĀMERE, RĀHOROI, RĀTAPU
}

enum class MaoriColoursByRgbEnum(val rgb: Int) {
    WHERO(0xFF0000),
    KARAKA(0xFFA500),
    KŌWHAI(0xFFFF00),
    KĀKĀRIKI(0x00FF00),
    KIKORANGI(0x0000FF),
    TŪĀURI(0x4B0082),
    PAPURA(0x800080)
}

Tests:

@Test
fun `enum elements have a name`() {
    MaoriNumbersEnum.TAHI.name
    MaoriNumbersEnum.TAHI.name shouldBe "TAHI"
}

@Test
fun `enum elements have an ordinal`() {
    MaoriDaysEnum.RĀAPA.ordinal shouldBe 2
}

@Test
fun `subsequent element are greater than previous elements`() {
    MaoriDaysEnum.RĀAPA shouldBeGreaterThan MaoriDaysEnum.RĀTŪ // Weds > Tues
}

@Test
fun `enum elements can have a value`() {
    MaoriColoursByRgbEnum.KĀKĀRIKI.rgb shouldBe 0x00FF00
}

Pretty self-explanatory I think.

What am I currently using these for? Well in my WordCollections class I have this sort of thing:

class WordCollections {
    companion object {
        enum class MI(val value: String) {
            ONE("tahi"),
            TWO("rua"),
            THREE("toru"),
            FOUR("wha"),
            // …
        }

        val listOfMaoriNumbers = listOf(
            MI.ONE.value,
            MI.TWO.value,
            MI.THREE.value,
            MI.FOUR.value,
            // …
        )
        
        // …
    }
    // …
}

And a test for that listOfMaoriNumbers is:

@Test
fun `It should return a list of Maori numbers`() {
    val numbers = WordCollections.listOfMaoriNumbers
    numbers.size shouldBe  10
    numbers[0] shouldBe "tahi"
    numbers[1] shouldBe "rua"
    numbers[2] shouldBe "toru"
    numbers[3] shouldBe "wha"
    // …
}

All it really means is I have an ordered set of uniform "tokens" that I can use to represent Maori numbers, and anywhere I wanna say "wha", I can use MI.FOUR.value; and MI.FOUR is greater than MI.THREE etc. Obvs it's a contrived usage, but that's the idea. They're sets of ordered constants, basically.


when

when is Kotlin's take on switch. It's important to note it can be used as either an expression or a statement. Here's an example of it used as an expression:

@Test
fun `It should return the correct value`() {
    val en = "one"
    val mi = translateNumber(en)
    mi shouldBe "tahi"
}

private fun translateNumber(en: String): String {
    val mi = when (en) {
        "one" -> "tahi"
        "two" -> "rua"
        "three" -> "toru"
        "four" -> "wha"
        else -> throw IllegalArgumentException("Unknown number $en")
    }
    return mi
}

The when construct is assigning a value, so it's an expression.

Also note this error I am causing if I comment out the else:

What does it mean by "be exhaustive"? Here's an example:

@Test
fun `It should return the correct value for a colour`() {
    val mi = MaoriColoursEnum.KĀKĀRIKI
    val en = translateColour(mi)
    en shouldBe "green"
}

private fun translateColour(mi: MaoriColoursEnum): String {
    val en = when (mi) {
        MaoriColoursEnum.WHERO -> "red"
        MaoriColoursEnum.KARAKA -> "orange"
        MaoriColoursEnum.KŌWHAI -> "yellow"
        MaoriColoursEnum.KĀKĀRIKI -> "green"
        MaoriColoursEnum.KIKORANGI -> "blue"
        MaoriColoursEnum.TŪĀURI -> "indigo"
        MaoriColoursEnum.PAPURA -> "violet"
    }
    return en
}

Here I have a case for every single possible value of mi (it has to be one of the colours defined in MaoriColoursEnum), so there is no possibility of an else situation arising.

This shows one of the other benefits of using enum types: it gives control over which values are possible in a situation. I can't specify MaoriColoursEnum.PARAURI (brown), because it's not defined in MaoriColoursEnum. So as well as an enum being a "set" of stuff, it's a bounded set of stuff.

OK, so what's a when statement? It's just when one doesn't assign a value from it. Maybe the cases perform actions rather than derive a value, eg:

@Test
fun `It should print the correct English day for the passed-in Maori day`() {
    val rāmere = MaoriDaysEnum.RĀMERE
    val output = SystemLambda.tapSystemOut {
        printEnglishDayFromMaori(rāmere)
    }
    output shouldBe "Friday"
}

private fun printEnglishDayFromMaori(mi: MaoriDaysEnum) {
    when (mi) {
        MaoriDaysEnum.RĀHINA -> print("Monday")
        MaoriDaysEnum.RĀTŪ -> print("Tuesday")
        MaoriDaysEnum.RĀAPA -> print("Wednesday")
        MaoriDaysEnum.RĀPARE -> print("Thursday")
        MaoriDaysEnum.RĀMERE -> print("Friday")
        MaoriDaysEnum.RĀHOROI -> print("Saturday")
        MaoriDaysEnum.RĀTAPU -> print("Sunday")
    }
}

Here I'm printing, not returning a value. It's a semantic difference, really. At least I could not find any other meaningful difference, from googling about. I only mention it at all because the docs do.

Right. That's the very first koans exercise of the day covered. Next…


Sealed classes

Reminder: by default classes in Kotlin are closed by default. The cannot be extended. If one make a class open, then anything can extend it. If one makes a class sealed then one can extend it, but only by subclasses in the same package.

I'm gonna use a sealed interface in this example (because it makes more sense to me), but the same applies to sealed interfaces.

First I have this lot in its own package:

package junit.language.classes.fixtures.package1

sealed interface SomeSealedInterface

class SomeSealedImplementation(val property: String) : SomeSealedInterface {
    fun useThisMethod(): String {
        return property
    }
}

class SomeOtherSealedImplementation(val property: String) : SomeSealedInterface {
    fun useThisOtherMethod(): String {
        return property
    }
}

This is a daft interface & implementation, I know; but bear with me.

@Test
fun `A sealed class does not need an else in a when`() {
    fun handleSomeSealedInterfaceInstance(obj: SomeSealedInterface): String {
        return when (obj) {
            is SomeSealedImplementation -> obj.useThisMethod()
            is SomeOtherSealedImplementation -> obj.useThisOtherMethod()
        }
    }
    val testObj = SomeSealedImplementation("some property value")

    val result = handleSomeSealedInterfaceInstance(testObj)

    result shouldBe "some property value"
}

The key bit here is not so much what I'm testing, it's that the when expression doesn't need an else option. The compiler knows that the only two possible options for a SomeSealedInterface are one of either a SomeSealedImplementation or a SomeOtherSealedImplementation: there can be no other options. If it wasn't a sealed interface, then anything else could implement it, so I'd either need to handle that here, or have an else.

The docs say this, which makes a kind of sense:

In some sense, sealed classes are similar to enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances, each with its own state.

I checked what happens if I tried to use that sealed interface in a different package:

So… err… that's a long way of saying "nope". Cool.


Extension functions

One can bolt new functionality into an existing class:

@Test
fun `an extension function can be called on an instance of a class`() {
    class TranslationPair(val en: String, val mi: String)

    fun TranslationPair.getBoth(): String {
        return "$en $mi"
    }

    val green = TranslationPair("green", "kākāriki")

    val result = green.getBoth()
    result shouldBe "green kākāriki"
}

Here I've added a getBoth method to my TranslationPair.

It seems one can also do this to inbuilt classes:

@Test
fun `an extension function can be added to an inbuilt Kotlin class`() {
    fun String.toMaori(): String {
        return when (this) {
            "one" -> "tahi"
            "two" -> "rua"
            "three" -> "toru"
            "four" -> "wha"
            else -> throw IllegalArgumentException("Unknown number $this")
        }
    }

    val result = "four".toMaori()
    result shouldBe "wha"
}

Here I've added a toMaori method to the String class. I'm gonna file this under "good to know: don't do it".

The docs go to lengths to point out that the extension function is not actually monkeying with the original class, it's all just hocus-pocus in the way the function is called. Fair cop. But for one's implementation code, it looks the same. Handy enough, I think. If used cautiously.

There's also Other Stuff one can do with extension functions - and extension properties - but nothing that I thought I could meaningfully expand on here that the docs don't do a fine job of. Just go RTFM ;-).


That's the end of that next section of koans exercises, and I feel like a break from Kotlin, so I'll end here for now.

Code for this article is on GitHub @ Tag 1.17, specifically:

Righto.

--
Adam

Sunday 16 October 2022

Kotlin / Ktor: G'day world from a Docker container

G'day:

Not sure what this article is going to end up being about. However I am hovering over the "New Project" button in IntelliJ, and am gonna attempt to at least get to a "G'day world" sort of situation with a Ktor-driven web service today.

Why Ktor

We need to port our monolithic CFML/CFWheels app to a more… erm… forward-thinking and well-designed solution. The existing app got us to where we are, and pays our salaries, but its design reflects a very "CFML-dev" approach to application design. We've decided to shift to Kotlin, as you know. We also need to adopt some sort of framework to implement the application atop-of, and we've chosen Ktor for a few reasons:

  • It's focus is micro-services and small footprint.
  • From what I've read, it focuses on being a framework instead of being an opinion-mill, how other frameworks can tend to be.
  • It's written for Kotlin; unlike say Spring, which is written for Java and it shows. We're using Kotlin to have the benefits of the JVM, but to steer clear the Java Way™ of doing things.
  • It's created by JetBrains, who created Kotlin, so hopefully the Ktor design team with be aligned with the Kotlin design team, so it should be a pretty Kotlin-idiomatic way of doing things.
  • Support for it is baked-in to IntelliJ, so it's a "first class citizen" in the IDE.

Also basically we need to pick something, so we're cracking on with it. If we do some quick investigation and it turns our Ktor ain't for us: I'd rather know sooner rather than later.

Let's get on with it.


Project

One can create a new Ktor project via IntelliJ ("New Project"):

I've only filled in the situation-specific stuff here, and left everything else as default. I've clicked the "Create Git repository" option: I hope if gives me the option to provide a name for it before it charges off and does it, cos I don't want it just to be called "gdayworld". So I might back out of that choice if it doesn't work for me:

Let's press "Next"…

Argh! I have to make decisions! I haven't even finished my first coffee of the day yet!

There are roughly one million plug-ins on offer here, and I don't even know what most of them are. For now, all I need this thing to do is to have testing for a greeting endpoint that says "G'day world" or something, so I doubt I'll need most of this stuff. Let's have a scan through.

OK, I've selected these ones:

  • Routing
  • DefaultHeaders
  • CallLogging
  • CallId
  • kotlinx.serialization - this also required the ContentNegotiation plug-in

After clicking "create" it got on with it, downloaded some stuff, built the project and declared everything was fine. I now have this lot:


Baseline checks

Right, let's see what tests it installed by default:

package me.adamcameron

import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.plugins.callloging.*
import org.slf4j.event.*
import io.ktor.server.request.*
import io.ktor.server.plugins.callid.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlin.test.*
import io.ktor.server.testing.*
import me.adamcameron.plugins.*

class ApplicationTest {
    @Test
    fun testRoot() = testApplication {
        application {
            configureRouting()
        }
        client.get("/").apply {
            assertEquals(HttpStatusCode.OK, status)
            assertEquals("Hello World!", bodyAsText())
        }
    }
}

Most of those imports aren't necessary btw, that's a wee bit sloppy. It only claims to need these ones:

import io.ktor.http.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlin.test.*
import io.ktor.server.testing.*
import me.adamcameron.plugins.*

I'll leave it as-is for now. The test looks sound actually. Well: I've purposely not looked at the code yet, but a test that tests that a GET to / returns "Hello World!" seems reasonable. Let's run it:

Cool. OK, let's run the app then, given it looks like it'll work:

C:\Users\camer\.jdks\semeru-11.0.17\bin\java.exe […]
2022-10-16 11:01:49.119 [main]  INFO  ktor.application - Autoreload is disabled because the development mode is off.
2022-10-16 11:01:49.205 [main]  INFO  ktor.application - Application started in 0.148 seconds.
2022-10-16 11:01:49.205 [main]  INFO  ktor.application - Application started: io.ktor.server.application.Application@57312fcd
2022-10-16 11:01:50.448 [DefaultDispatcher-worker-1]  INFO  ktor.application - Responding at http://127.0.0.1:8080  

It ran. Does it actually respond on http://127.0.0.1:8080?

Cool. OK, so I have a test that passes an an app that works. Gonna push that to GitHub as v0.2 (v0.1 was the empty repo). And I'm gonna have a shufti around the files it's created and see what's what.


Tweaking

OK, I'm not gonna look at those old-school xUnit-style tests. I'm gonna adapt them to use the more declarative BDD style I've been using so far when testing stuff with Kotlin. So this means I'm going to add some Kotest dependencies. The test is now:

@DisplayName("Tests of the / route")
class ApplicationTest {
    @Test
    fun `Tests the root route responds with the correct status and message`() = testApplication {
        application {
            configureRouting()
        }
        client.get("/").apply {
            status shouldBe HttpStatusCode.OK
            bodyAsText() shouldBe "Hello World!"
        }
    }
}

I'm also refactoring the class name and location to src/test/kotlin/acceptance/IndexRouteTest.kt. It's not testring the app, it's testing the route. Plus it's an acceptance test, and I wanna keep those separate from unit tests / integration tests etc (poss premature optimisation here I guess). I've also lost the subdirectory structure from /src/main/kotlin/me/adamcameron/Application.kt to be just /src/main/kotlin/Application.kt. Kotlin's own style guide recommends this:

In pure Kotlin projects, the recommended directory structure follows the package structure with the common root package omitted. For example, if all the code in the project is in the org.example.kotlin package and its subpackages, files with the org.example.kotlin package should be placed directly under the source root, and files in org.example.kotlin.network.socket should be in the network/socket subdirectory of the source root.

Next I feel there's a design bug in the index route, but I'm gonna push my current tweaks first, and sort that out in the next section.


Giving control to a controller

This design bug: here's the entirety of the implementation of that index route and its handling:

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

That's in /src/main/kotlin/plugins/Routing.kt

Routing should limit itself to what it says ion the tin: routing. It should not be providing the response. It should route the request to a controller which should control how the response is handled. I know this is only example code, but example could should still follow appropriate design practices. So erm: now I have to work out how to create a controller in Ktor. I'm pleased I have a green test on that index route though, cos this is all pretty much a refactoring exercise, so whatever I do: in the end I'll know I have done good if the test still passes.

Hrm. Having not found any examples in the Ktor docs of how to extract controller code out of the routing class, I found myself reading Application structure, specifically these paras:

Different to many other server-side frameworks, it doesn't force us into a specific pattern such as having to place all cohesive routes in a single class name CustomerController for instance. While it is certainly possible, it's not required.
Frameworks such as ASP.NET MVC or Ruby on Rails, have the concept of structuring applications using three folders - Model, View, and Controllers (Routes).

My emphasis. I see. Ktor does not separate-out the idea of routing from the idea of controllers, I see. To me they're different things, but I guess I can see there's overlap. I'm not hugely enamoured with their thinking that "despite the rest of the world using the term MVC, we know better: we're gonna think of it as MVR". Just… why. If you wanna conflate routing and controllers, yeah fine. But in that case they conflate into the controller part of MVC. You don't just go "ah nah it's MVR, trust me". Remember what I said before about opinionated frameworks? This is why I don't like it when frameworks have opinions.

But anyway.

We can still separate out groups of "route-handlers" (sigh) into separate functions. ow I have this:

package routes

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.indexRouting() {
    route("/") {
        get {
            call.respondText("Hello World!")
        }
    }
}

And my original configureRouting function is just this:

fun Application.configureRouting() {

    routing {
        indexRouting()
    }
}

That's good enough.


Auto-reload

One good thing my RTFMing about controllers lead me to was how to get my app to rebuild / reload when I make code changes. By default every time I changed my code I had to shut down the app (remember it's serving a web app now), rebuild, and then re-run the app. That was not the end of the world, but was pretty manual.

Ktor have thought about this, and the solution is easy.

First, I tell my app it's in development mode (in gradle.properties):

junitJupiterVersion=5.9.0
kotestVersion=5.5.0
kotlinVersion=1.7.20
ktorVersion=2.1.2
logbackVersion=1.2.11

kotlin.code.style=official

org.gradle.warning.mode=all

development=true

This in turn is picked up by code in build.gradle.kts

application {
    mainClass.set("ApplicationKt")

    val isDevelopment: Boolean = project.ext.has("development")
    applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

(that code was already there).

Then I needed to tell the app what to pay attention to for reloading (in Application.kt):

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0", watchPaths = listOf("classes")) {
        configureMonitoring()
        configureSerialization()
        configureRouting()
    }.start(wait = true)
}

classes there is a reference to build/classes in the project file system.

Then get Gradle to rebuild when any source code changes:

PS C:\src\kotlin\ktor\gdayworld> ./gradlew --continuous :build
BUILD SUCCESSFUL in 1s
13 actionable tasks: 13 up-to-date

Waiting for changes to input files... (ctrl-d then enter to exit)
<-------------> 0% WAITING
> IDLE

And another instance of Gradle to run the app with it watching the build results:

PS C:\src\kotlin\ktor\gdayworld> ./gradlew :run                  
> Task :run
2022-10-16 14:21:19.374 [main]  DEBUG ktor.application - Java Home: C:\apps\openjdk\EclipseAdoptium
2022-10-16 14:21:19.374 [main]  DEBUG ktor.application - Class Loader: jdk.internal.loader.ClassLoaders$AppClassLoader@73d16e93:...]
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\me\adamcameron\plugins for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\routes for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\me for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\META-INF for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\me\adamcameron for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\plugins for changes.
2022-10-16 14:21:19.562 [main]  INFO  ktor.application - Application started in 0.298 seconds.
2022-10-16 14:21:19.562 [main]  INFO  ktor.application - Application started: io.ktor.server.application.Application@5a45133e
2022-10-16 14:21:19.937 [main]  INFO  ktor.application - Responding at http://127.0.0.1:8080
<===========--> 85% EXECUTING [15s]
> :run

When I change any source code now, the project rebuilds, and the app notices the recompiled classes, and restarts itself:

modified: C:\src\kotlin\ktor\gdayworld\src\main\kotlin\routes\IndexRoutes.kt
Change detected, executing build...


BUILD SUCCESSFUL in 6s
13 actionable tasks: 12 executed, 1 up-to-date

Waiting for changes to input files... (ctrl-d then enter to exit)
<=============> 100% EXECUTING [8m 55s]
2022-10-16 14:26:20.438 [eventLoopGroupProxy-4-2]  INFO  ktor.application - 200 OK: GET - /
2022-10-16 14:26:34.861 [eventLoopGroupProxy-3-1]  INFO  ktor.application - Changes in application detected.
2022-10-16 14:26:35.073 [eventLoopGroupProxy-3-1]  DEBUG ktor.application - Changes to 18 files caused application restart.
[...]
2022-10-16 14:26:35.106 [eventLoopGroupProxy-3-1]  INFO  ktor.application - Application auto-reloaded in 0.012 seconds.
2022-10-16 14:26:35.106 [eventLoopGroupProxy-3-1]  INFO  ktor.application - Application started: io.ktor.server.application.Application@33747fec
2022-10-16 14:26:35.107 [eventLoopGroupProxy-4-2]  INFO  ktor.application - 200 OK: GET - /
<===========--> 85% EXECUTING [5m 32s]
> :run

Note how the app doesn't restart until I actually use it, which is good thinking.

One might as why I have dropped down to a shell to do this autoload stuff? As far as I can tell it's not baked into IntelliJ yet, so needs to be handled directly by Gradle for now. It's not a hardship. I mean: the shells I am running there are being run from within IntelliJ, it's just slightly more complicated than a key-combo or some mouseclicks.

OK. That's all good progress. I'm gonna take a break and come back and create my own controller / response / etc, which is what the object of the exercise was today.


Docker

Ktor's way

I was not expecting this to be the next step, but I just spotted some stuff about Docker in the Ktor docs ("Docker"), so I decided to see what they said.

[time passes whilst I do battle with the docs]

OK, screw that. It's a very perfunctory handling of it. I can build a jar and create an image that will run it, and then run the container - and it all works - but it's… a bit… "proof of concept". From reading the docs and the code snippets that link from the docs (Deployment - Ktor plugin › Build and run a Docker image).

I had to add this to my build.gradle.kts file:

ktor {
    fatJar {
        archiveFileName.set("gday-world-ktor.jar")
    }
    docker {
        jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)
        localImageName.set("gday-world-ktor")
        imageTag.set("${project.version}-preview")
        portMappings.set(listOf(
            io.ktor.plugin.features.DockerPortMapping(
                8080,
                8080,
                io.ktor.plugin.features.DockerPortMappingProtocol.TCP
            )
        ))
    }
}

And then from the shell I could run this lot:

PS C:\src\kotlin\ktor\gdayworld> ./gradlew :buildFatJar     
[…]
PS C:\src\kotlin\ktor\gdayworld> ./gradlew :runDocker

And I would indeed end up with a running Docker container. Which is handy, but I had no control over what params were passed to docker run, so I couldn't even give the container a name, so I just ended up with one of Docker's random ones. That's a bit amateurish. I checked to see if I was missing anything with the plugin, but this is the code (from Ktor's repo on GitHub):

private abstract class RunDockerTask : DefaultTask() {
    @get:Inject
    abstract val execOperations: ExecOperations

    @get:Input
    abstract val fullImageName: Property<String>

    @TaskAction
    fun execute() {
        val dockerExtension = project.getKtorExtension<DockerExtension>()
        execOperations.exec {
            it.commandLine(buildList {
                add("docker")
                add("run")
                for (portMapping in dockerExtension.portMappings.get()) {
                    add("-p")
                    with(portMapping) {
                        add("${outsideDocker}:${insideDocker}/${protocol.name.lowercase()}")
                    }
                }
                add(fullImageName.get())
            })
        }
    }
}

It looks to me like it simply builds a string docker run [port mappings] [image name], and that's it. No scope for me to specify any other of docker run's parameters in my build config.

So: nah, not doing that; I'll DIY. It's at least shown me what I need to do in a DockerFile and I can organise my own docker-compose.yml file.


My way

[…]

I have this docker/Dockerfile:

FROM gradle:7-jdk17 AS build
COPY --chown=gradle:gradle .. /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle test --no-daemon
RUN gradle buildFatJar --no-daemon

FROM openjdk:17
EXPOSE 8080:8080
RUN mkdir /app
COPY --from=build /home/gradle/src/build/libs/*.jar /app/gday-world-ktor.jar
ENTRYPOINT ["java","-jar","/app/gday-world-ktor.jar"]

This is pretty much lifted from the Ktor Docker › Prepare Docker image docs I linked to above, I've just added the test-round in first.

And this docker/docker-compose.yml:

version: '3'

services:
  gday-world-ktor:
    build:
      context: ..
      dockerfile: docker/Dockerfile
    ports:
      - "8080:8080"
    stdin_open: true
    tty: true

And when I run docker-compose up --build --detach, after a couple of minutes, I get an up and running container with my app in it. Bonus: it halts if my tests don't first pass.

I'm not enamoured with the "after a couple of minutes" part of this: seems really slow for what it needs to do. I am "sure" there must be a way of telling Gradle to do the build, test-run and jar-build all on one operation. However I'm over googling things starting with "gradle" today, so I'm gonna leave it for now.

I'm pretty happy with the progress I made today.

Righto.

--
Adam

Friday 14 October 2022

Kotlin: looking at delegated properties

G'day:

In my "Kotlin/Gradle: abstracting versions into a config file, and wondering what delegated properties are" article from a few days ago, I landed upon some syntax in my build.gradle.kts file that I didn't quite get:

val kotestVersion: String by project
val systemLambdaVersion: String by project
val commonsLang3Version: String by project
// …

I wasn't so sure what that by project jazz was all about. project is just an object as it turns out - the properties of which are defined in gradle.properties - and this by thing is something to do with "Delegated properties. Let's have a look at those.

The intro in the docs there say this:

With some common kinds of properties, even though you can implement them manually every time you need them, it is more helpful to implement them once, add them to a library, and reuse them later.

Erm: fine. But I am none the wiser. I'll try some code. Well: I'll get Copilot to try some code, as it turns out.


Property delegates

Here's a test

fun `It can delegate the Maori translation of a word`() {
    val one = Number(1)
    one.mi shouldBe "tahi"
    one.en shouldBe "one"
}

Different from my usual test Number classes, this one only sets the digit. The rest… gets done for me. How?

class Number(val id: Int) {
    val mi: String by MaoriNumber()
    val en: String by EnglishNumber()
}
class MaoriNumber {
    operator fun getValue(thisRef: Number, property: kotlin.reflect.KProperty<*>): String {
        return when (thisRef.id) {
            1 -> "tahi"
            2 -> "rua"
            3 -> "toru"
            4 -> "wha"
            5 -> "rima"
            6 -> "ono"
            7 -> "whitu"
            8 -> "waru"
            9 -> "iwa"
            10 -> "tekau"
            else -> "unknown"
        }
    }
}

(EnglishNumber is the same, just in English).

I've specified the getValue function as an operator because the docs told me to, btw. Well in fact Copilot wrote 95% of that for me, and then I checked back with the docs to make sure it nailed it. Copilot wanted MaoriNumber to extend Number, for some reason, which is unnecessary so I removed that. It did everything else. It makes sense: it receives the object it's a delegate for, and the property of the object it's a delegate for, looks up the id value in its internal map and returns the value.

Next I want to throw an exception for unsupported values, instead of returning "unknown". I have NFI how to do this, so I am gonna encourage Copilot to help out.

fun `It throws an exception when the Maori translation is not found`() {
    val eleven = Number(11)
    shouldThrow<IllegalArgumentException> {
        eleven.mi
    }.message shouldBe "No Maori translation for 11"
}

I had to type as far as "It throws and exception", then Copilot did the rest. Except it did it for "2" not "11", which was a slight disappointment. The code supports "2".

The key thing to notice here is that the exception doesn't get thrown until the relevant property is accessed; not when the object is first created. mi is not initialised until it's needed.

The update for the delegate is reasonably obvious:

7 -> "whitu"
8 -> "waru"
9 -> "iwa"
10 -> "tekau"
else -> throw IllegalArgumentException("No Maori translation for ${thisRef.id}")

Copilot handled it after "throw", but did not work out to correlate the message back to the test. This is fine. Let's see if it nails the English version now that I've explained stuff better:

7 -> "seven"
8 -> "eight"
9 -> "nine"
10 -> "ten"
else -> throw IllegalArgumentException("No English translation for ${thisRef.id}")

Nailed it. It could not work out that I wanted to include an expectation for this in the test, so I hand-cranked that.

That's quite cool.

Seems there are some more specialised delegate types as well…


Lazy properties

A property that delegates to lazy will use lazy's lambda to initialise the property first time round,

@Test
fun `It is only initialise when accessed`() {
    class TestClass {
        val lazyProperty: String by lazy {
            telemetry.add("lazyProperty initialised")
            "lazyProperty"
        }
        val telemetry: MutableList<String> = mutableListOf("initialising telemetry")
    }

    val testClass = TestClass()
    testClass.telemetry.first() shouldBe "initialising telemetry"
    testClass.lazyProperty shouldBe "lazyProperty"
    testClass.telemetry.last() shouldBe "lazyProperty initialised"
    testClass.telemetry.size shouldBe 2
    testClass.lazyProperty shouldBe "lazyProperty"
    testClass.telemetry.size shouldBe 2
}

Note for this:

  • Lazy properties cannot be mutable, so have to be vals, and obviously can't have a setter. If I try I get this error in IntelliJ:

    Type 'Lazy<TypeVariable(T)>' has no method 'setValue(DelegatedPropertiesTest.LazyProperties.TestClass, KProperty<*>, String)' and thus it cannot serve as a delegate
  • The telemetry tells the story here. Only the first access to lazyProperty's getter actually executes the code in the lazy lambda: there is no second telemetry entry for the second access to it.

I've augmented this test in the version of this code in source control, but whilst it demonstrates some stuff, this first iteration is clearer, hence using this one.

This next test shows that if the lazy initialisation code fails, it will re-try subsequent times:

@Test
fun `It can throw an exception on initialisation but will continue to attempt to initialise the property on subsequent calls`() {
    class TestClass {
        private var initAttempts = 0

        val lazyProperty: String by lazy {
            initAttempts++
            telemetry.add("lazyProperty initialisation called ($initAttempts)")
            if (initAttempts <= 2) {
                throw IllegalStateException("lazyProperty initialisation failed ($initAttempts)")
            }
            telemetry.add("lazyProperty initialisation succeeded ($initAttempts)")
            "lazyProperty"
        }
        val telemetry: MutableList<String> = mutableListOf("initialising telemetry")
    }

    val testClass = TestClass()
    testClass.telemetry[0] shouldBe "initialising telemetry"

    shouldThrow<RuntimeException> {
        testClass.lazyProperty
    }.message shouldBe "lazyProperty initialisation failed (1)"

    testClass.telemetry.size shouldBe 2
    testClass.telemetry[1] shouldBe "lazyProperty initialisation called (1)"

    shouldThrow<RuntimeException> {
        testClass.lazyProperty
    }.message shouldBe "lazyProperty initialisation failed (2)"
    testClass.telemetry.size shouldBe 3
    testClass.telemetry[2] shouldBe "lazyProperty initialisation called (2)"

    shouldNotThrow<RuntimeException> {
        testClass.lazyProperty
    }
    testClass.telemetry.size shouldBe 5
    testClass.telemetry[3] shouldBe "lazyProperty initialisation called (3)"
    testClass.telemetry[4] shouldBe "lazyProperty initialisation succeeded (3)"

    shouldNotThrow<RuntimeException> {
        testClass.lazyProperty
    }
    testClass.telemetry.size shouldNotBeGreaterThan 5
    testClass.telemetry.last() shouldBe "lazyProperty initialisation succeeded (3)"
}

This looks like a lot of code, but there's not much to it.

  • The lazy initialisation lambda throws an exception the first two times it's called.
  • After that, it initialises the value successfully.
  • The test calls the getter 1, 2, 3, 4 times, and checks what telemetry has been logged each time.
  • After the third call (which was successful), the lambda is not called again, ie: there is no further telemetry since the first successful initialisation.

Observable properties

This sounded interesting to me when I saw the heading ("Ooh! Baked-in Observer Pattern!"), but I was underwhelmed by the implementation.

inner class TestClass {
    var observableProperty: String by Delegates.observable("initial value") {
        property, oldValue, newValue ->
        telemetry.add("${property.name} changed from $oldValue to $newValue")
    }
    val telemetry: MutableList<String> = mutableListOf("initialising telemetry")
}

Test:

@Test
fun `It shows that observable properties can be used to track changes to a property`() {
    val testClass = TestClass()

    testClass.telemetry.first() shouldBe "initialising telemetry"
    testClass.observableProperty shouldBe "initial value"
    testClass.telemetry.size shouldBe 1

    testClass.observableProperty = "new value"
    testClass.telemetry.size shouldBe 2
    testClass.telemetry[1] shouldBe "observableProperty changed from initial value to new value"
    testClass.observableProperty shouldBe "new value"

    testClass.observableProperty = "newer value"
    testClass.telemetry.size shouldBe 3
    testClass.telemetry[2] shouldBe "observableProperty changed from new value to newer value"
    testClass.observableProperty shouldBe "newer value"

    testClass.observableProperty = "newer value"
    testClass.telemetry.size shouldBe 4
    testClass.telemetry[3] shouldBe "observableProperty changed from newer value to newer value"
    testClass.observableProperty shouldBe "newer value"
}

So… you can have a setter method. I… already knew that. How does that observableProperty property differ from this:

var standardProperty : String = "initial value"
    set(value) {
        telemetry.add("standardProperty changed from $field to $value")
        field = value
    }

I got Copilot to write the test for me, and it's the same as the observableProperty one :-(

To be clear, that seems all there is to it. One can only "attach" that single listener to the "observable" property. There's no mechanism to go observableProperty.attach({property, oldValue, newValue -> /* stuff here */}) to attach more handlers later, which would be handy.

Hopefully I'm missing something, but googling about, I get the same vibe from other people too.


Vetoable properties

These were only mentioned in passing on that previous docs page I had been looking at. I only found out about them because I asked Copilot "anything else about delegated properties I should know about?", and it autofilled something about vetoable properties, so did a google.

class TestClass {
    var vetoableProperty: String by Delegates.vetoable("initial value") {
        property, oldValue, newValue ->
        telemetry.add("${property.name} changed from $oldValue to $newValue")
        newValue != "veto value"
    }
    val telemetry: MutableList<String> = mutableListOf("initialising telemetry")
}

Here if the lambda returns false, the property doesn't actually get updated:

val testClass = TestClass()
testClass.telemetry.first() shouldBe "initialising telemetry"
testClass.vetoableProperty shouldBe "initial value"
testClass.telemetry.size shouldBe 1

testClass.vetoableProperty = "new value"
testClass.telemetry.size shouldBe 2
testClass.telemetry[1] shouldBe "vetoableProperty changed from initial value to new value"
testClass.vetoableProperty shouldBe "new value"

testClass.vetoableProperty = "veto value"
testClass.telemetry.size shouldBe 3
testClass.telemetry[2] shouldBe "vetoableProperty changed from new value to veto value"
testClass.vetoableProperty shouldBe "new value"

testClass.vetoableProperty = "newer value"
testClass.telemetry.size shouldBe 4
testClass.telemetry[3] shouldBe "vetoableProperty changed from new value to newer value"
testClass.vetoableProperty shouldBe "newer value"

See how after trying to set it to "veto value", the value was not updated from the previous one.


Setting properties from a map

Probably just the tests speak for themselves here, as I think it's pretty obvious what's going on, and why/how:

fun `It shows that map properties can be used to access a map by key`() {
    class Number(val map: Map<String, Any?>) {
        val id: Int by map
        val en: String by map
        val mi: String by map
    }

    val number = Number(
        mapOf(
            "id" to 1,
            "en" to "one",
            "mi" to "tahi"
        )
    )
    number.id shouldBe 1
    number.en shouldBe "one"
    number.mi shouldBe "tahi"
}
@Test
fun `It shows that map properties can be used to access a map by key with a default value from a function`() {
    class Number(val map: Map<String, Any?>) {
        val id: Int by map
        val en: String by map
        val mi: String by map
        val ga: String by map.withDefault { key -> "unknown $key" }
    }

    val number = Number(
        mapOf(
            "id" to 1,
            "en" to "one",
            "mi" to "tahi"
        )
    )
    number.id shouldBe 1
    number.en shouldBe "one"
    number.mi shouldBe "tahi"
    number.ga shouldBe "unknown ga"
}
@Test
fun `It shows that map properties can be used to access a map by key with a default value from a function that throws an exception`() {
    class Number(val map: Map<String, Any?>) {
        val id: Int by map
        val en: String by map
        val mi: String by map
        val ga: String by map.withDefault { key -> throw IllegalArgumentException("unknown key $key") }
    }

    val number = Number(
        mapOf(
            "id" to 1,
            "en" to "one",
            "mi" to "tahi"
        )
    )
    number.id shouldBe 1
    number.en shouldBe "one"
    number.mi shouldBe "tahi"
    shouldThrow<IllegalArgumentException> {
        number.ga
    }.message shouldBe "unknown key ga"
}

Copilot wrote all that code for me btw. I changed the third language from the French it suggested to Irish.


OK, so that's that. The concept of property delegation is interesting / useful, But I didn't quite "get" why they have bothered with the pre-packaged ones (lazy, observation, vetoable),if I'm honest. I suspect I am missing something still…

The code is on GitHub, here: DelegatedPropertiesTest.kt.

Righto.

--
Adam