diff options
Diffstat (limited to 'posts/First Impressions of the Kotlin Programming Language/first-impressions-of-the-kotlin-programming-language.org')
| -rw-r--r-- | posts/First Impressions of the Kotlin Programming Language/first-impressions-of-the-kotlin-programming-language.org | 494 |
1 files changed, 0 insertions, 494 deletions
diff --git a/posts/First Impressions of the Kotlin Programming Language/first-impressions-of-the-kotlin-programming-language.org b/posts/First Impressions of the Kotlin Programming Language/first-impressions-of-the-kotlin-programming-language.org deleted file mode 100644 index 75b7438..0000000 --- a/posts/First Impressions of the Kotlin Programming Language/first-impressions-of-the-kotlin-programming-language.org +++ /dev/null @@ -1,494 +0,0 @@ -#+TITLE: First Impressions of the Kotlin Programming Language -#+DATE: <2018-12-17 Mon> -#+TAGS: opinion, programming, java, kotlin, android - -In the introduction of the previous post I wrote for this series, [[http://jakob.space/posts/first-impressions-of-the-rust-programming-language/][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. |