home

First Impressions of the Kotlin Programming Language

December 17, 2018 ❖ Tags: opinion, programming, java, kotlin, android

In the introduction of the previous post I wrote for this series, First Impressions of the Rust Programming Language, I alluded to the presence of arguments that programming language safety should be achieved by moving to languages such as Java which run on a virtual machine. While "safety" may no longer be the first thing that comes to mind in discussion of these languages, especially with the hundreds1 of vulnerabilities in various implementations of the Java virtual machine, it would be unfair to deny that the principle of running programs in a sandboxed virtual machine is safer than running machine code directly. This post won't be making any claims about safety, though, as I'm more interested in writing about my impressions from a language design perspective. So, how does Java fare in this regard?

My first run-in with Java was when I was 16 and still in high school; I had enrolled in AP computer science, of which the curriculum was and still is taught using Java. I already knew Python and C at that point, but enrolled anyway as I was never given a formal computer science education, and because the idea of university credits was enticing to me. The portion of the class that regarded concepts of computer science – algorithm design and data structures – was a blast, but I quickly became frustrated with the programming assignments because of Java's horribly unwieldy nature. Despite my distaste for the way the course was taught, my grades stood out enough that the teacher approached me about working as a teaching assistant the following year. I took him up on the offer, and suffered. The position involved troubleshooting students' issues during lab periods, and nearly every time I sat down to help a student, the issue was with one of Java's numerous pitfalls rather than a conceptual misunderstanding of computer science. At of the time of writing this, I have just completed my first semester of university, and Java is apparently inescapable; I have been forced to use it once more in my introductory data structures course. It has not grown on me at all in these past three years, and I am not exaggerating when I say that the language has literally given me nightmares.

Refusing to use Java outside of my coursework would seemingly prevent me from programming for any platform which makes use of Java, such as Android, but it turns out that there's an alternative: using a language that compiles down to the same bytecode as Java does. For those not in the know, Java works by compiling source code ahead of time into a Java class file, which is a container format for JVM bytecode. Bytecode is comparable to assembly, but it doesn't run on actual hardware.2 Instead, it runs on an abstract machine implemented in software. JVM bytecode as a compiler target is not a new concept; there are plenty of languages targeting the JVM specifically, such as Groovy, Scala, and Clojure (the latter of which I hope to cover in a future post), but Kotlin was the first I heard of.

While targeting Android is what I ended up using Kotlin for, my initial reason for learning it was not nearly as practical. For context, I go between phases of absolutely hating everything related to Java and phases of almost being able to tolerate it, but that tolerance is mostly out of curiosity about the implementation – the most recent case of piqued interest coming about through obtaining the J2ME JAR for Doom RPG and having an inclination to indulge in some retro Java (circa JRE 1.3) reverse engineering. Really, I wanted to learn Kotlin so that I could compare the bytecode generated by the Kotlin compiler to the bytecode generated by past and present Java compilers,3 but I won't be talking about that much in this post.

Throughout this post, I'll be treating Kotlin as a language that only runs on the JVM, but it's worth noting that the compiler has recently gained support for targeting Javascript and LLVM as well, so this perspective doesn't fully represent the language.

As my first foray into writing nontrivial4 Kotlin, I decided to write a version of Minesweeper, my favorite logic puzzle. The code is available on sr.ht. Implementing Minesweeper might be a bit redundant as F-Droid already has a GPLv3'd implementation of Minesweeper in its repositories, but that one's implemented in C++, QML, and Javascript.

For the purposes of illustration, here's a simplified implementation of the Minesweeper logic sans Android API:

data class Tile(val adjacentMines: Int = 0, val mine: Boolean = false, val masked: Boolean = true)

/**
 * The Minesweeper "grid", containing instances of [Tile].
 */
class Grid(val width: Int, val height: Int, val tiles: Array<Tile>) {
    constructor(width: Int = 8, height: Int = 8, mines: Int = 10)
    : this(width, height, Array<Tile>(width * height) { Tile() }) {
        for (i in 0 until mines) {
            val x = (0 until width).random()
            val y = (0 until height).random()
            placeMine(x, y)
        }
    }

    private fun index(x: Int, y: Int) = y * width + x
    private fun valid(x: Int, y: Int) = y in 0 until height && x in 0 until width
    private fun place(x: Int, y: Int, tile: Tile) {
        tiles[index(x, y)] = tile
    }

    /**
     * Returns the tile at the given coordinates.
     *
     * @throws IllegalArgumentException if the X coordinate is outside
     * the range of [0, width), or if the Y coordinate is outside the
     *  range of [0, height).
     */
    operator fun get(x: Int, y: Int) = if (valid(x, y)) {
        tiles[index(x, y)]
    } else {
        throw IllegalArgumentException("Invalid coordinates (${x}, ${y})")
    }

    /**
     * Places a mine at the given coordinates.
     *
     * @throws IllegalArgumentException if the X coordinate is outside
     * the range of [0, width), or if the Y coordinate is outside the
     *  range of [0, height).
     */
    fun placeMine(x: Int, y: Int) {
        if (!valid(x, y)) {
            throw IllegalArgumentException("Invalid coordinates (${x}, ${y})")
        }

        if (this[x, y].mine) {
            return;
        }

        place(x, y, Tile(mine = true))

        val xMin = (x - 1).coerceAtLeast(0)
        val xMax = (x + 1).coerceAtMost(width - 1)
        val yMin = (y - 1).coerceAtLeast(0)
        val yMax = (y + 1).coerceAtMost(height - 1)

        for (y in yMin..yMax) {
            for (x in xMin..xMax) {
                with (tiles[index(x, y)]) {
                    if (!mine) {
                        place(x, y, copy(adjacentMines + 1))
                    }
                }
            }
        }
    }

    /**
     * Reveals a tile at the given coordinates according to the game rules.
     *
     * @throws IllegalArgumentException if the X coordinate is outside
     * the range of [0, width), or if the Y coordinate is outside the
     *  range of [0, height).
     */
    fun reveal(x: Int, y: Int) {
        if (!valid(x, y) || !this[x, y].masked) {
            return;
        }

        with (this[x, y]) {
            place(x, y, copy(masked = false))

            if (!mine && adjacentMines == 0) {
                val xMin = (x - 1).coerceAtLeast(0)
                val xMax = (x + 1).coerceAtMost(width - 1)
                val yMin = (y - 1).coerceAtLeast(0)
                val yMax = (y + 1).coerceAtMost(height - 1)

                for (y in yMin..yMax) {
                    for (x in xMin..xMax) {
                        reveal(x, y)
                    }
                }
            }
        }
    }

    override fun toString() = buildString {
        for (y in 0 until height) {
            for (x in 0 until width) {
                val tile = this@Grid[x, y]
                append(when {
                    tile.masked -> "."
                    tile.mine -> "M"
                    tile.adjacentMines == 0 -> " "
                    else -> tile.adjacentMines.toString()
                } + " ")
            }
            append("\n")
        }
    }
}

fun main(args: Array<String>) {
    val grid = Grid()

    for (i in 0 until 9) {
        val x = (0 until 8).random()
        val y = (0 until 8).random()
        grid.reveal(x, y)
        println(grid)
    }
}

And just within the first line, we're introduced to a feature that provides massive gains in readability over Java. Data classes.

data class Tile(val adjacentMines: Int = 0, val mine: Boolean = false, val masked: Boolean = true)

In Java, if you want to group a few related values together, you'd probably write a full implementation of a class with "getter" and "setter" methods, and potentially other methods for things like testing for equality. That's a whole new file (assuming that you want to use this structure between classes) and several lines of code for something that should really be expressed in one line. Kotlin allows for structures to be declared this way. Here, we define a Tile structure, which acts as an immutable container for the information we want to associate with a tile in the Minesweeper grid. The compiler automatically derives methods to check for equality (equals), to create a unique hash code (hashCode), to provide a string representation (toString), and to make a copy of the structure. That last point on making copies brings me to another feature of Kotlin that makes programming with immutable data structures a breeze: named and optional parameters. Notice that there are default values in the above declaration – adjacentMines is 0, mine is false, and masked is true. If I wanted to create a tile that was a mine, but was still masked, I could simply call Tile(mine = true). Java only supports positional overloading, so in Java, it would be new Tile(0, true), assuming that I had an overloaded constructor with a default masked value. Returning to the point on the automatically-generated copy method, the named and optional parameters really shine here. Take a look at how it's used in the reveal method:

with (this[x, y]) {
    place(x, y, copy(masked = false))

I should probably explain the with statement to fully unpack what's happening here. with essentially allows us to run a block of code in the scope of an object. copy is a method of this[x, y], but we don't need to write this[x, y].copy since we are in the class scope of Tile (the type of this[x, y]).

The code for initializing a new Grid object does a nice job of illustrating Kotlin's ranges.

for (i in 0 until mines) {
    val x = (0 until width).random()
    val y = (0 until height).random()
    placeMine(x, y)
}

Kotlin is similar to Python in that there are no longer C-styled for loops (initialization + condition + afterthought). Instead, there are iterator-based for loops, and enumeration is done with ranges. a..b represents the range of integers from [a, b], and a until b represents the range from [a, b). Ranges are also objects, which is why we can write something like (0 until width).random(), which picks a random integer in the range [0, width).

Another big feature is type inference, which eliminates another pain in reading and writing Java. Take this facetious example:

InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState myState = new InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState();

The class name shows up twice on the same line, which I personally think is absurd. Is the type of myState really not obvious from the rvalue?

val myState = InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState()

Ah, much better. Of course, sometimes variables need type information, such as in the case of function parameters and return values. Actually, the type of a return value can be inferred, too. This can be seen in the definitions of index and valid.

private fun index(x: Int, y: Int) = y * width + x
private fun valid(x: Int, y: Int) = y in 0 until height && x in 0 until width

Functions can be written this way if their body is a single expression, and the compiler can infer the type of the return value from that expression. Expressions are a big thing in Kotlin. if is an expression (which you can see in the implementation of get), much like it is in Rust, as is when – Kotlin's replacement to Java's switch (which is sadly lacking in the way of pattern-matching). Assignment, fortunately, is not an expression like it is in C and Java.5 Here's an example of when used as an expression:

override fun toString() = buildString {
    for (y in 0 until height) {
        for (x in 0 until width) {
            val tile = this@Grid[x, y]
            append(when {
                tile.masked -> "."
                tile.mine -> "M"
                tile.adjacentMines == 0 -> " "
                else -> tile.adjacentMines.toString()
            } + " ")
        }
        append("\n")
    }
}

This is yet another example of a function body being written as a single expression. buildString is a function in the Kotlin standard library that takes a lambda as a parameter,6 executes it in the context of a Java StringBuilder, and returns the result of building that string. Kotlin provides a number of facilities to make working with strings more pleasant, including string interpolation:

val myNum = 7
return "myNum is ${myNum}" // --> "myNum is 7"

One last feature from the example above – Kotlin supports operator overloading. I'm sure that there was some rationale behind omitting operator overloading from Java, but I'm a proponent of languages that offer support for it. Where the operator overloading occurs in the example might not have been obvious, though.

operator fun get(x: Int, y: Int) = if (valid(x, y)) {
    tiles[index(x, y)]
} else {
    throw IllegalArgumentException("Invalid coordinates (${x}, ${y})")
}

get corresponds to the indexing notation, which is why this[x, y] has shown up a few times in the code. Yes, the indexing notation can take multiple parameters. The names that Kotlin associates with different operators tends to draw parallels with the conventions of the Java standard library. In the case of get, this means that you can use indexing notation on a Map. Pretty neat.

There are a few other features that I think are worth mentioning, but don't appear in the Minesweeper example.

Explicit type conversion

Kotlin lacks implicit type coercion, which I think is a huge benefit in terms of readability. While it isn't as much of an issue in Java, having to explicity mark type conversion is an excellent way of avoiding issues with type juggling. Again, my opinions here have largely been shaped by my experience as a teaching assistant.

Explicit nullability

This is probably the crowning feature of Kotlin: a solution to "The Billion Dollar Mistake" that is null. I'm actually not a fan of explicit nullability as a solution, since I prefer the use of an Option type like in Rust.7 That said, it does put the type system to work enforcing null safety at compile time, and it is a lot more pleasant than dealing with null Java, so I'd call it a win.

Basically, a variable can be null if its type is suffixed with a ?. For example, Int? can be null, but Int can't. An expression of type Int? must be checked for null before it can be used, which can be done in a number of ways. The most simple being to make use of another Kotlin feature: "smart casts".

val myNum: Int? = null

if (myNum != null) {
    println("${myNum + 4}") // myNum has been casted from Int? to Int at this point.
}

There are other ways, too. Kotlin has a null-coalescing operator, a "not null" assertion, and so on. Explicit nullability is definitely a pain when starting out, though. In Jouri Mamaev's "Why Kotlin Sucks", the following issue is described as a "histerically-useless war with nullable."

var value : Int? = null

fun F() : Int {
  if (value != null) return 0
  return value // Compiler error: "Smart cast to 'Int' is impossible, because 'value' is a mutable property that could have been changed by this time"
}

Mamaev goes on to show an example using the aforementioned "not null" assertion, but the way I prefer to deal with this issue (other than not having variables that are both nullable and mutable) is:

var value : Int? = null

value?.let {
    it.something()
}

let will capture value and pass it to a lambda, allowing you to run some code without having to worry about the value changing from under you. I have no idea if this is idiomatic or not, but it works for me.

Of course, Kotlin was meant to interoperate with existing Java code, which has little notion of explicit nullability, so the benefits of explicit nullability go out the window more often than not.

No more checked exceptions

I'm sorry, but I really don't want to write about how much I hate checked exceptions in Java. If you aren't familiar with them, but still want to know what they are, I'd suggest looking for an article elsewhere. Otherwise, all you need to know is that they aren't an issue in Kotlin.

== for value equality as opposed to reference equality

This comes back to the operator overloading feature, == calls out to equals. I do have a slight problem with this, though. === is used for reference equality. I'm thankful that the operator exists, but this is completely orthogonal to what Javascript does and I think that this might be a barrier for anyone coming from there. Again, this is a slight problem; I think the gains in clarity from using == for value equality outweigh the awkward === operator.

Whew, listing all of the features that make Kotlin a better choice than Java is exhausting. This is one of the complaints that I have about the language: it's massive in scope. Of course, this is a consequence of Java having an absurd number of warts that Kotlin tries to mend, but learning the ins and outs of Kotlin is a significant undertaking. This is a shortened list of the features I enjoyed but didn't mention in this post:

  • Lambdas having access to variables that are not final.
  • Classes and methods being final by default.
  • Inner classes being static by default.
  • Properties (declaring 'get' and 'set').
  • Unpacking.
  • Spreading.
  • Collection literals.
  • Common I/O functions such as println being included in the prelude.
  • Top-level visibility.
  • Module visibility.
  • Named imports.
  • Infix functions.
  • Nested functions.
  • Decorator classes with by.
  • Anonymous objects being able to implement multiple interfaces.
  • Immutable collections.
  • Lazy evaluation.
  • with and apply expressions.
  • Safe type casting.
  • Lazy member initialization.
  • Function inlining.
  • Support for DSL creation.

I could seriously just go on for days. There's a lot to keep in your head all at once.

Other than the huge scope, there are few things I'd say I dislike about Kotlin. Sure, there are things I wish it had, like Rust-styled variable shadowing, but nothing drives me up the wall or anything. kotlin-mode feels like it was put together in a few hours and compilation times are miserable, but other than that, Kotlin is a solid language.

Also, I'll forgo talking about the community and the ecosystem. It's just completely transparent to me right now. From what I can gather, it seems to be pretty corporate (kotlin.link links to a Linkedin group, Google+, Slack…), which might be because of its use case in Android development, but whatever.

To conclude, Kotlin's alright. It isn't a miracle of language design, but it's designed in a way that makes it easy to map onto the JVM. In that sense, it's pragmatic. I'm not excited about it, but it's levels beyond Java in terms of how tolerable it is, so I'll take it. Well, for Android development, at least. For projects where I'm not wrestling with a Java-based platform, I'll use something more fun. To me, Rust is like Marshmallow Froot Loops, and Kotlin is like Cheerios.8

I'd also like to take a minute to thank everyone who's given feedback on the previous post. I really appreciate all the suggestions! I've put Ada and Pony on my list of languages to cover in the future, and hopefully I'll be able to make some progress on shrinking that list now that I'm done with the semester and finally have some free time.

Footnotes:

2

Well, that statement is only partially true. ARM processors can execute JVM bytecode in hardware, and I would not be surprised if there are other chips out there with similar capabilities.

3

Not that it would help me at all in reversing Doom RPG, I was just curious about bytecode.

4

I would typically consider Minesweeper to be somewhat trivial, but implementing it for Android was not an easy task.

5

I do think that assignment as an expression can occasionally afford some clarity, especially in the way of C, but in my time as a teaching assistant, I can say that I have seen its usage be erroneous more often than clever.

6

In case it is unclear from the code, lambdas can be passed to functions sans parentheses. This is just syntactic sugar.

7

Java 8 actually has an Optional type, but people seem to dislike it. Wonder why…

8

This analogy is painfully overused, but it just can't be beat.

Comments for this page

    Click here to write a comment on this post.