summaryrefslogtreecommitdiff
path: root/posts/First Impressions of the Kotlin Programming Language/first-impressions-of-the-kotlin-programming-language.org
diff options
context:
space:
mode:
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.org494
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.