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