brainbaking/content/post/2021/08/the-kotlin-rose-tinted-glas...

5.3 KiB

title date categories tags
My Kotlin Rose-Tinted Glasses Broke 2021-08-19T11:03:00+02:00
programming
java
kotlin

Earlier this month, I explored Kotlin and concluded that it was Java 2.0, but still Java. My fingers produced nothing but Kotlin code the last few weeks, and I feel like I have to further nuance my initial enthusiasm for the new (primarily) JVM-based language. You see, the more I programmed in Kotlin, the more I realized I was still programming in Java. And the more I wanted to interact with Java APIs, the more I had to twist and turn in order to get things done the way I wanted to.

My Kotlin rose-tinted glasses broke.

I am not saying I developed a strong disliking for the language: rather, I'm starting to miss Go's simplicity. But for Android-based app development, Kotlin is the recommended way to go, so I plodded on. Perhaps more cautiously. As you compile your Kotlin code, you generate JVM-compatible bytecode. The problem is, sometimes, that generated code is not what you want. Some situations even reminded me of troublesome times where we once thought relying on Spring Boot and scaffolding was a great idea. Code generation is always asking for trouble, especially if that duplication hurts your eyes and you'd like to refactor it out.

A few simple examples to demonstrate the issue. Suppose I want to write a static method on a class. That's impossible: Kotlin does not know static. The JVM does, however, and thus, translations are in order, and you have to learn the new syntax, which basically does the same thing:

data class Kitten(val name: String, val age: Double) {
    companion object {
        fun fromCatery(catery: String): Kitten =
            Kitten("sir-$catery-a-lot", 1.0)
    }
}

Nothing too troubling, right? As explained before, the data class comes with a bunch of free stuff, and things in companion object... Well... We can get used to that. The trouble is, it's longer than static and it pops up just about everywhere. Another problem is that since name and age are automatically converted to properties with getters, we can't tell Kotlin to create a simple field instead. Or can we?

class AnotherKitten {
    @JvmField
    val name: String
}

This can be mixed with the companion object. And then there's also @JvmStatic, @JvmOverloads, and so forth. All these things simply exist to precisely instruct the Kotlin compiler how it should write... Java code? An extra annotation perhaps wouldn't hurt, but many of these tricks---which, if used excessively, can damage the readability of your code---make you wonder whether or not you should simply revert to "plain old Java" instead. Or Go, of course.

Another thing that irritates me is return values and this scoping of so-called scope functions: let, run, with, apply, and also. The "helpful" table that indicates which one references what object, returns what value, and is an expression function or not, indicates that, while its use reduces Java plumbing clutter, it introduces another problem instead. Is that really that much better? I get it, each of those utility functions does something different and can be used in different situations: sometimes you'd like to preserve this from the outer scope---without relying on even weirder things like the qualified this expression---and sometimes you don't want to return the context. Judge for yourself:

fun main() {
    val miauw = AnotherKitten().apply {
        // object ref = this
        purrNoise = 100.2
        mood = "naughty"
        // returns context
    }
    val woof = Doggie("Snoopy").let { // it -> is optional
        // object ref = it
        it.barkAt(miauw)
        it.runAwayFrom(miauw)
        return it // returns lambda result
    }
}

Sure, it beats writing Java-style setter after setter:

AnotherKitten miauw = AnotherKitten();
miauw.setPurrNoise(100.2);
miauw.setMood("naughty");

But then again, where is your constructor? Speaking of which, how do you call stuff from a primary constructor in Kotlin? That's right, use an init {} block. A what? Why? Because the "constructor" is the part between brackets on the first line of a class definition. Again, it's no big deal once you're used to bit, but once you start mixing a primary constructor with a secondary one and an init block, perhaps a simple Java constructor is the more elegant alternative.

class Doggie(val name: String) {
    constructor(cat: Cat) : this("${cat.name}-turned-dog")

    init {
        callSomeMethod()
    }
}

There are ample examples akin to the ones I briefly mentioned above. You have to be on your toes with Java interoperability. After dipping a(nother) toe in Kotlin's coroutines, the code admittedly looks neat and compact, but the generated bytecode is ugly as hell and it's easy to make mistakes compared to Go's much more elegant goroutine system. Of course, Go does not need to drag the entire Java history along with it. Furthermore, Go-to-C interop wasn't as smooth as I expected it to be, compared to Kotlin-to-Java.

Still, it has become very apparent for me that Kotlin does not reduce Java's complexity, it only hides it. And sometimes not very well.