summaryrefslogtreecommitdiff
path: root/posts/first-impressions-of-the-kotlin-programming-language.org
diff options
context:
space:
mode:
Diffstat (limited to 'posts/first-impressions-of-the-kotlin-programming-language.org')
-rw-r--r--posts/first-impressions-of-the-kotlin-programming-language.org494
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.