Monday, July 11, 2022

Code: Kotlin First Impressions

Introduction
Kotlin is one of the newest and most popular JVM languages and it might be one of my new favorite languages. Disclaimer: I haven't written much production-ready code in it yet.

As a JVM language like Scala, Groovy, and Clojure; Kotlin tries to take the portability of the Java Virtual Machine and familiarity of the Java language while improving upon its archaic design with the conciseness and power of modern programming languages. Simply put: it's Java but better—for real this time!

Scala was my favorite attempt at improving Java, but I was pretty unhappy with the build system. Since Kotlin is written by JetBrains, an IDE company, compiler and IDE support is first-class. You can even use JetBrains' IntelliJ to output Kotlin code to Java, and vice versa. For this reason alone, it is easy to pick up for Java developers.

Improvements on Java 8
So what does Kotlin improve upon? Firstly, Java's verbosity. Compare a hello world program in Java:

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

and in Kotlin:

fun main() {
    println("Hello world!")
}

The first thing to note is that no class definition is required. You can simply add top-level functions. Kotlin is a functional language, so a function definition is required, but the public access modifier is implied. Top-level functions are always static. And type inference also applies to functions, so no void is required here. Just fun to mark that it's a function. In the actual function itself, println is a top-level function, so no need to qualify the function with a package and class. Lastly, no semicolons.

This is not some neat trick about hello world programs, the rest of the language is written to be concise. Record classes (data classes), raw strings, type inference, default function parameter values, string interpolation, collection builders, array slices, range operators, and null safety operators all help make your code very concise and efficient. If you don't know what those mean take the following Kotlin code:

fun doSomething(param: String = "default") {
  val stringTemplate = "$param ${1 + 2} String"

  val nums = (0..20).toList() // A list of integers from 0 to 20
  println(nums.slice(1..5 step 2)) // Prints "[1, 3, 5]"

  val newStr: String? = getStringOrNull()
  println(newStr?.length) // Prints length or "null" if string is null

  val neverNull = getStringOrNull2() ?: "" // Converts nulls to ""

  val regex = """\w+@gmail\.com"""
}

Doing this in Java 8 without special libraries would be like:

public static void doSomething() {
  doSomething(null);
}

public static void doSomething(String param) {

  param = (param == null) ? "default" : param;
  String stringTemplate = String.format("%s %d String", param, (1 + 2));

  List<Integer> nums = IntStream.rangeClosed(0, 20).boxed()
      .collect(Collectors.toList()); // It's either this or a for loop

  // A for loop is simpler than the stream version
  List<Integer> slicedList = new ArrayList<>();

  for (int i = 1; i <= 5; i += 2) {
    slicedList.add(nums.get(i));
  }
  System.out.println(slicedList); // Prints "[1, 3, 5]"

  String newStr = getStringOrNull();

  System.out.println((newStr == null) ? null : newStr.length());

  // We need two lines for this, unless we call the function twice
  String intermediateVar = getStringOrNull2();
  String neverNull = (intermediateVar == null) ? "" : intermediateVar;

  String regex = "\\w+@gmail\\.com"; // We must escape special characters
}

Or at least that's my interpretation. With Java 8 streams, there are a few functions to create and manipulate collections. You can also short circuit nullable objects with Optional.ofNullable([nullable]).orElse([default]), but I prefer the ternary operator in Java.

Another great feature is destructuring declarations. In Python, you can return multiple values. In Kotlin, we get half of that feature by being able to "destructure" a returned value into multiple variables:

  val (name, age) = person
  for ((key, value) in map) {
    // stuff
  }

Lastly, it is interoperable with Java. You can call Java code from Kotlin and you can even call Kotlin code from Java. One of Java's biggest strengths is the massive set of libraries you can use. These libraries are available, too, in Kotlin.

Drawbacks
Sounds too good to exist? Well, Kotlin has some drawbacks. I probably will come up with more after using the language for a while, but for right now, the drawbacks are minimal.

For one, variable type declarations follow the variable name as in var a:Int. For many of us, we started programming in C, C++, or Java where variable types come before the variable name. I disliked this seemingly arbitrary change until it was explained to me.

When declaring a variable, the variable's name is the most important aspect. When your language has type inference, the type isn't always written. This means variables declarations are generally aligned well, regardless of type declaration or lack thereof. This alignment is maintained for variables (val or var) and functions (fun).

val a = 1   // Infers type from value
val b: Int  // No value to infer type
var c: String = "String" // Optionally specify type
// Infer return type of single-expression function
fun sum(arg1: Int, arg2: Int) = arg1 + arg2

Notice that the val/var/fun keywords are all aligned vertically, as are the names. So while this name-type order will take some getting used to, it's not a bad thing. Pretty much all modern languages (Go, Rust, Swift, Scala, Nim, and Python) use it.

One issue that I dislike is the amount of scope functions. let, run, with, apply, and also are all ways to call a block of code with a temporary scope and each is used slightly differently. This seems like overkill and I don't imagine I'll ever memorize which is which. You don't want to have too many ways to do the same thing in a language, as it makes reading and reviewing code difficult.

Overall, it seems like a very fun, concise language and an upgrade to Java.

Kotlin vs Newer Versions of Java
JetBrains 2021 Developer Study


Having said that, Java has progressed quite past version 8. Java 8 is still the most popular version, according to a JetBrains survey, being used by 72% of Java programmers (users were allowed to select multiple versions). SNYK's survey results says Java 11 slightly outweighs Java 8 with both being around 61%. In either case, the higher versions of Java have little use with both companies saying Java 15 use is around 13%.


SNYK 2021 Developer Study

Java 19 will drop in September, but for a company looking for stability should probably stick with Java 17, as it is will still be the most recent version with Long-Term Support from Oracle. So what does Java 17 add to the language that Java 8 and Java 11 users may be unfamiliar with? I will describe some of the notable language updates, ignoring preview features and compiler and JVM enhancements.

Java 9 to 11 updates:

  • 9: Project Jigsaw: Modular system
    Introduces a module system to the language to define exports and dependencies.
  • 9: Private methods in interfaces
  • 9: Actual immutable collections
    Allows things like Set.of(item1, item2).
  • 10: Type inference
    Introduces var keyword.
  • 10: Root certificates.
    Allows TLS out of the box.
  • 11: HttpClient
    A replacement for HttpUrlConnection.
  • 11: More String and Files methods
    New methods like String::lines and Files::readString.
Java 12 to 17 updates:
  • 12: New methods in String, Files, and Collectors
  • 12: A number formatter
  • 14: Switch expressions
    Makes switch expressions much less verbose.
  • 15: Text blocks
    Allows setting Strings to nicely-formatted multiline blocks of text without inserting pesky "\n".
  • 16: Records
    Allows the creation of data classes with default getters and setters in essentially one line.
  • 16: Pattern matching for instanceof
    Decreases verbosity by allowing the declaration of a variable in an instanceof expression.
  • 17: Sealed classes
    Increases control over class inheritance.
Most of this article was written with Java 8 in mind. It's fair to say that Java has come a long way since then and Java now has more syntactic sugar. Specifically, type inference, text blocks (multiline strings), and records (data classes). However, even these new features are weaker than their Kotlin equivalents:
  • Type inference only applies to local variables, not top-level variables or lambda expressions
  • Text blocks aren't raw strings and will still process escape sequences (i.e. you still have to escape all of your backslashes). Writing regex in Java still sucks.
  • A record cannot contain any private instance variables. Admittedly, this is a tiny disadvantage.
Even the work that has been done to modernize Java doesn't bring it up to par with Kotlin. That's why I'm excited to start using Kotlin.