Kotlin v. Scala: The JVM Showdown

Nov 16, 2017

So there's a new language craze going around the internet, a new JVM alternative by the makers of the popular Intellij IDEA, which goes by the name of Kotlin. Now, I'm not particularly a fan of Intellij - I'm an experienced Scala user, and I much prefer Eclipse or even VSCode over IDEA. Up until now, I'd passed over Kotlin as just another Scala knockoff, not worth the time to look into deeper. But with so much attention being put on Kotlin, I finally decided to take a look and see what all the rage is about.

Keep in mind that this isn't a definitive guide to the differences between Kotlin and Scala - these are my personal observations and opinions on the two, and what makes Kotlin worth (or not worth) the switch. I will also be focusing primarily on the differences between the two - I won't be covering the numerous great things that both do over Java, or their (in my opinion) failings in comparison to Java.

The Good Parts

One of the first things I noticed about Kotlin was that it definitely does some things right. There are some features in Kotlin that are just a royal pain to do in Scala, but come so naturally to Kotlin that it honestly astounded me. Let's start with the good stuff, shall we?

Null Safety

This is by far my favorite thing about Kotlin over Scala - Kotlin has astounding null safety. Scala relies primarily on an Option[T] type to deal with null safety. This has it's own advantages and disadvantages, but it often leads to a lot of code like this:

def parse(from:String):Stream[Token] = from.headOption {
  case None => Stream.empty
  case Some(c) => c match {
      // ...
  }
}

There's nothing wrong with it, persay - but it is quite verbose. Kotlin, on the other hand, bakes null safety directly into the type system. Take, for example, the String type. In Scala, this value could be null, and you have to handle that. In Kotlin, however, this type quite literally cannot be null. In order to represent a nullable string, you would need the String? type. This forces you to think ahead of time about every possible way something could be null, and gives you an effective and simple way to deal with it.

Furthermore, Kotlin's compiler is extremely intelligent about this. In both Scala and Java, even if you know that a value isn't null, you still have to either handle it being null or unbox an Option[T] type. This leads to a lot of boilerplate code, wrapping and unwrapping types to handle the off chance that a value might be null. Kotlin, on the other hand, knows when you've checked that something isn't null. As soon as you hit code where you've already checked that a variable isn't null, the compiler automatically typecasts it to a non-nullable type, and you no longer have to deal with the possibility of null. That ends up looking something like this:

fun getName(person:Person?):String? {
  // return person.name    // (Compile error: cannot access property of nullable)
  if(person == null) return null
  return person.name
}

Kotlin even provides some utilities to make this less boilerplatey, in the form of the null safe access and Elvis operators.

// Elvis operator
fun getName(person:Person?):String? {
  person ?: return null
  return person.name
}

// Null safe access
fun getName(person:Person?):String? = person?.name

All of this makes handling nullable values in Kotlin an absolute breeze compared to the boilerplate of Scala's Option[T], let alone Java's incessant null checks.

Easy Typecasting

Continuing on from the above, Kotlin's hyper-intelligent type checker doesn't just extend to null safety. Any time you do a type check, from then on in that scope that type check is remembered and reused. For example:

// Assuming Geek is a subclass of Person
fun getName(person:Person):String {
  if(person is Geek) return person.name + " (Geek level:" + person.geek_level.toString() + ")"
  // println(person.geek_level)    // (Compile error: geek_level is not a property of Person)
  return person.name
}

In fact, a ClassCastException is almost impossible to achieve thanks to Kotlin's "smart casting". Nicely done, Intellij!

Inextensibility by Default

Kotlin and Scala both encourage the use of immutable variables ("values") wherever possible. This makes it easier to reason about the program you're working with by making sure you know what a value will be, for sure without question.

What Kotlin gives beyond Scala, however, is inextensibility by default. This is something that I wasn't so sure about when I first tried Kotlin, but I quickly realized just how good it is. Essentially, Kotlin prevents you from extending classes, overriding methods, etc., unless they're specifically designated as either open, abstract, or something that's implicitly abstract like a trait.

This seems at first to go against the grain of a statically typed, object-oriented language; not being able to extend a class? What even? But after a few minutes of working with it, it becomes abundantly clear why.

The big advantage of being inextensible by default is complete and total control over who and what can access your code. In Scala, you have to annotate everything that outside code shouldn't be able to touch with private, making it easy to forget to hide something that shouldn't be available, such as internal implementation details. Kotlin, on the other hand, forces you to think about what should be available, rather than what shouldn't. It ensures that you make good, intentional decisions about what outside code can see, rather than accidentally exposing something you shouldn't have.

Extension Functions

On the other end of the spectrum, Kotlin allows for something called "extension functions". This is essentially Kotlin's equivalent of Ruby's monkey-patching, and used well can make your program a lot more efficient and concise. This is something that Scala doesn't really have as such - the closest you can get is an implicit conversion to a wrapper class that provides the new method.

Java-like Syntax

One thing I noticed about Kotlin that Scala lacks in a lot of cases is a willingness to reuse Java's syntax. For example, Kotlin isn't afraid to have code laying around like this:

sealed open data class Polar(val radius:Int, val angle:Int) : Point

Whereas Scala relies a lot more on a tight, operator-based syntax:

case class Polar(val radius:Int, val angle:Int) < Point

Perhaps it's just me, but both have their advantages - I like the conciseness of Scala, but the readability of Kotlin is pretty nice too.

Extendable Case Data Classes

One of my biggest gripes with Scala is that case classes can't be extended, period. Kotlin's data classes, which are the rough equivalent, can be, albeit with some slight restrictions. Simple as that. In fact, the primary difference between the two examples in the last section is that the Kotlin class can be extended by classes in the same file, while the Scala one cannot.

Conversion from Java

I haven't experienced this personally, but apparently switching from Java to Kotlin is an absolute breeze, especially for users of Intellij IDEA. Having experienced the switch from Java to Scala before, this is definitely a plus for Kotlin.

The Bad Parts

Kotlin is far from perfect though. It has it's own quirks and flaws, just like any language. There are a lot of features of Scala that I missed bitterly in Kotlin during my short stay, just like there are a lot of features of Kotlin that I really wish Scala would borrow.

No Implicit Conversions

This was really the big dealbreaker for me. One of the beautiful things about Scala is the ability to define implicit conversions that will automatically convert one type to another using a predefined method. While this can cause issues if abused, it makes DSL syntax beautifully easy and sweeps a lot of annoying, boilerplatey unboxing under the rug.

Kotlin forgoes the power and expressiveness of implicit conversions entirely, however. While their reason is valid (implicit conversions, if abused, can confuse and complicate things greatly), the language loses out on a lot of power that could be gained.

The closest thing Kotlin provides is extension functions, which cover a lot of the basic use cases - however, they come with their own drawbacks. It's nearly impossible to call the original method from within an overriding extension function, let alone outside of it. You can also only extend a class with methods, rather than with anything you want via implicit conversion to a wrapper class.

For more complicated uses, such as a JSON DSL for example, you essentially have to run through a pre-processor stage. For example, in order to learn Kotlin I replicated as closely as possible the AST of my Scala JSON library, ScJson, in Kotlin, and ended up with this code:

fun obj(vararg pairs:Pair<String, Any?>) = JsonObject(pairs.map {(k,v) -> Pair(k, json(v))}.toMap())
fun arr(vararg elems:Any?) = JsonArray(elems.map(::json))

fun json(v:Any?):JsonValue<*>? = when(v) {
  is JsonValue<*> -> v
  is String -> JsonString(v)
  is Int -> JsonInt(v)
  is Float -> JsonFloat(v)
  is Boolean -> JsonBoolean(v)
  null -> null
  else -> throw IllegalArgumentException()
}

While there is a certain beauty to this, in that the logic is consolidated into a couple localized areas, it was quite annoying to realize that I had to loop through and convert every value to a subtype of JsonValue manually, rather than just defining a half-dozen implicit conversions and having done with it.

No View Bounds

This one is pretty self explanatory - without implicit conversions, there isn't any need for view bounds. On the other hand, view bounds are one of the Scala features I find myself using almost constantly, and it's really quite a pain not to have them.

Subclasses of Sealed Classes Must Be Nested

This was just a slight annoyance, really, but when I went to define subclasses of a sealed class, Kotlin decided to yell at me that the subclasses had to be nested inside the parent. This led to a handful of redundant-feeling type aliases defined at the top of my file and a few extra levels of indentation that I wish I could have avoided. Apparently this is fixed in a newer version, but despite being on what I assume was the latest version, the compiler still complained. Go figure.

Inheritance Boilerplate

This is the flipside of being inextensible by default - there's a lot of boilerplate associated with actually making things extensible. Kotlin code quickly becomes polluted with an inordinate amount of opens and overrides. Not a big deal, just somewhat annoying over time.

No Operator Definitions

Another small gripe, but one that hamstrings DSLs in general - Kotlin doesn't allow you to define operators. You can override a small set of predefined operators (plus, minus, get, etc.), but you cannot define your own, and the set of operators that can be overriden is very small. The closest you get is the concept of infix functions, which allows you to define a function that can be used in infix position, like jwick arr "contacts" obj 0 v "name", for example. This isn't really a substitute for Scala's ability to define arbitrary operators, though - I much prefer a syntax like jwick | "contacts" | 0 $ "name" (and there's those view bounds coming into play again - I had to use two different functions in Kotlin, where one Scala operator did just fine).

... Java-like Syntax

This is a perfect example of something that can be great in some ways but annoying in others. Let's take a look at those two examples again:

sealed open data class Polar(val radius:Int, val angle:Int) : Point
case class Polar(val radius:Int, val angle:Int) < Point

... On second thought, I definitely prefer the conciseness.

Conclusions

So, is Kotlin the silver bullet the JVM has been waiting for, or does in pale in comparison to my beloved Scala? Well, the answer is....

Neither.

I know, I know, it seems like a wishy-washy, cop-out answer. But to be honest, it's the answer the programming community needs to hear, in my opinion. Scala certainly isn't perfect, and Kotlin certainly does a lot of things right. But neither is Kotlin this amazing silver bullet of a language. It has it's advantages and it's disadvantages, just like any other language.

Don't get me wrong, I'm not saying Kotlin is bad by any stretch. I'm just saying that maybe we shouldn't hail it as the savior the JVM has been waiting for. It's just another language, with it's own highs and lows. Enough language worshipping - find what works for you and use it.

With that said, I think I'll be sticking with Scala. I won't be afraid to employ Kotlin if I need to, and I certainly won't be considering it "just a Scala knockoff" anymore, but it just doesn't do it for me, unfortunately. If you think differently, more power to you - just so long as you don't try to indoctrinate me, we'll be fine =)