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
andapply
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:
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.
Not that it would help me at all in reversing Doom RPG, I was just curious about bytecode.
I would typically consider Minesweeper to be somewhat trivial, but implementing it for Android was not an easy task.
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.
In case it is unclear from the code, lambdas can be passed to functions sans parentheses. This is just syntactic sugar.
Java 8 actually has an Optional
type, but people seem to dislike it. Wonder why…
Comment form