diff options
Diffstat (limited to 'posts/first-impressions-of-the-kotlin-programming-language.org')
| -rw-r--r-- | posts/first-impressions-of-the-kotlin-programming-language.org | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/posts/first-impressions-of-the-kotlin-programming-language.org b/posts/first-impressions-of-the-kotlin-programming-language.org new file mode 100644 index 0000000..406d0a3 --- /dev/null +++ b/posts/first-impressions-of-the-kotlin-programming-language.org @@ -0,0 +1,494 @@ +#+TITLE: First Impressions of the Kotlin Programming Language +#+DATE: <2018-12-17 Mon 00:00> +#+TAGS: opinion programming java kotlin android + +In the introduction of the previous post I wrote for this series, [[http://jakob.space/blog/first-impressions-of-the-rust-programming-language.html][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 hundreds[fn:1] 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.[fn: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 [[https://en.wikipedia.org/wiki/Doom_RPG][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,[fn: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 nontrivial[fn:4] Kotlin, I decided to write a +version of Minesweeper, my favorite logic puzzle. The code is available on +[[https://git.sr.ht/~jakob/mines][sr.ht]]. Implementing Minesweeper might be a bit redundant as [[https://f-droid.org/en][F-Droid]] already has +a [[https://f-droid.org/en/packages/csd.qtproject.minesweeper][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: + +#+BEGIN_SRC kotlin :hl_lines 0 +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) + } +} +#+END_SRC + +And just within the first line, we're introduced to a feature that provides +massive gains in readability over Java. Data classes. + +#+BEGIN_SRC kotlin :hl_lines 0 +data class Tile(val adjacentMines: Int = 0, val mine: Boolean = false, val masked: Boolean = true) +#+END_SRC + +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: + +#+BEGIN_SRC kotlin :hl_lines 0 +with (this[x, y]) { + place(x, y, copy(masked = false)) +#+END_SRC + +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. + +#+BEGIN_SRC kotlin :hl_lines 0 +for (i in 0 until mines) { + val x = (0 until width).random() + val y = (0 until height).random() + placeMine(x, y) +} +#+END_SRC + +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: + +#+BEGIN_SRC java :hl_lines 0 +InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState myState = new InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState(); +#+END_SRC + +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? + +#+BEGIN_SRC kotlin :hl_lines 0 +val myState = InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState() +#+END_SRC + +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=. + +#+BEGIN_SRC kotlin :hl_lines 0 +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 +#+END_SRC + +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.[fn:5] Here's an example of =when= used as an expression: + +#+BEGIN_SRC kotlin :hl_lines 0 +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") + } +} +#+END_SRC + +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,[fn: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: + +#+BEGIN_SRC kotlin :hl_lines 0 +val myNum = 7 +return "myNum is ${myNum}" // --> "myNum is 7" +#+END_SRC + +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. + +#+BEGIN_SRC kotlin :hl_lines 0 +operator fun get(x: Int, y: Int) = if (valid(x, y)) { + tiles[index(x, y)] +} else { + throw IllegalArgumentException("Invalid coordinates (${x}, ${y})") +} +#+END_SRC + +=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 [[https://www.owasp.org/images/6/6b/PHPMagicTricks-TypeJuggling.pdf][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.[fn: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". + +#+BEGIN_SRC kotlin :hl_lines 0 +val myNum: Int? = null + +if (myNum != null) { + println("${myNum + 4}") // myNum has been casted from Int? to Int at this point. +} +#+END_SRC + +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 [[https://kukuruku.co/post/why-kotlin-sucks/]["Why Kotlin Sucks"]], the following issue is +described as a "histerically-useless war with nullable." + +#+BEGIN_SRC kotlin :hl_lines 0 +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" +} +#+END_SRC + +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: + +#+BEGIN_SRC kotlin :hl_lines 0 +var value : Int? = null + +value?.let { + it.something() +} +#+END_SRC + +=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 ([[https://kotlin.link/][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.[fn: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. + +[fn:1] Source: [[https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=Java+Runtime+Environment][Common Vulnerabilities and Exposures, MITRE]] + +[fn:2] Well, that statement is only partially true. [[https://en.wikipedia.org/wiki/Jazelle][ARM processors can execute JVM bytecode in hardware]], and I would not be surprised if there are other chips out there with similar capabilities. + +[fn:3] Not that it would help me at all in reversing Doom RPG, I was just curious about bytecode. + +[fn:4] I would typically consider Minesweeper to be somewhat trivial, but implementing it for Android was not an easy task. + +[fn: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. + +[fn:6] In case it is unclear from the code, lambdas can be passed to functions sans parentheses. This is just syntactic sugar. + +[fn:7] Java 8 actually has an =Optional= type, but people seem to dislike it. Wonder why... + +[fn:8] This [[https://en.wikipedia.org/wiki/Metacity#Aim][analogy]] is painfully overused, but it just can't be beat. |