Kotlin — Get Excited About Right Things

Jerzy Chałupski
7 min readJan 10, 2018

--

Probably the biggest news of the past year in Android world was the official Google support for the Kotlin programming language. After the announcement during Google I/O 2017, the blogosphere exploded with content. The quality of the content varied, but all of these blog posts, gists and tweets shared one emotion.

People were really excited about Kotlin.

The good thing is that getting excited about something is the first step on a way to do something great.

The bad thing is that I couldn’t shake the feeling that some of these people were setting themselves up for a big disappointment. I’ll risk coming off as a judgmental jerk and say they were getting excited about wrong things: a terseness of the language, clever tricks, syntactic sugars or trivialities, like lack of semicolons.

While all these Kotlin properties are nice, they add up to something you may call a superficial beauty of the programming language. Their appeal will quickly fade away, leaving you in the same place you were before (but with more complicated technical stack).

At the same time, I think the Kotlin has a lot more to offer. Look beyond the gimmicks and get excited about the right things.

Kotlin collections

The first feature I want to mention are the functional operators on collections. Instead of this:

List<Integer> veryImportantLogic(List<Integer> inputList) {
List<Integer> result = new ArrayList<>();
for (Integer input : inputList) {
if (input % 2 == 0) {
result.add(input);
}
}
return result;
}

You can write this:

fun veryImportantLogic(inputList: List<Int>): List<Int> {
return inputList.filter { it % 2 == 0 }
}

Most of the Kotlin blog posts that mention these features focus on the terseness of this syntax, and that’s exactly the kind of wrong focus I mentioned in the introduction. The true benefits become apparent when you need to change the transformation. The imperative code encourages merging everything into a single loop:

int veryImportantLogic(List<Integer> inputList) {
int result = 0;
for (Integer input : inputList) {
if (input == -1) break;
if (input % 2 == 0) {
result += input * input;
}
}
return result;
}

The functional code encourages a clear separation of each step, which improves readability and helps with the long-term maintenance of your codebase:

fun veryImportantLogic(inputList: List<Int>): Int {
return inputList
.takeWhile { it != -1 }
.filter { it % 2 == 0 }
.map { it * it }
.sum()
}

GOTCHA #1: intermediate allocations

While the snippet above looks nice, it’s terribly inefficient for large collections. Each transformation allocates the collection to hold the intermediate values, even though we’re interested only in the final result.

We can fix that by adding the asSequence call at the beginning of the method chain. It will turn each step into a decorator over input collection, greatly reducing the amount of allocated memory.

It’s especially important if the final call in the method chain is some form of reduce which merges the sequence into a smaller object, or call of a function like first, which short-circuits the processing.

Extension functions

The second mention worthy feature is related to the implementation details of the functional operators on collections.

Kotlin doesn’t use a wrapper class, like Guava’s FluentIterable. It’s not achieved by having a different collections class hierarchy than the rest of Java world. It’s not some runtime black sorcery or methodMissing kludge. The interop is perfect, everything works as if we’ve added the methods directly to java.util classes.

The feature I’m talking about are extension functions, which technically are just a syntactic sugar for standard utility methods.

The primary use case mentioned in Kotlin docs is fixing up a bad or incomplete APIs you don’t control. Designing a good API is very difficult, so almost certainly some APIs you use are giving you headaches, so just being able to tweak them to your needs justifies adding this feature to the programming language.

Things get even more interesting when you realize that you can create the extension functions that interact with other language features. The most beautiful example of this comes from Kotlin documentation:

val map: Map<*, *>

// without extensions
for (entry in map.entries) {
println("${entry.key}: ${entry.value}")
}

// using Map.iterator() and Map.Entry.component*() extensions
for ((key, value) in map) {
println("$key: $value")
}

The most interesting usages leverage the fact that extension functions are scoped. We can locally extend the API in a way that wouldn’t make sense globally or use some local data inside our extensions. The second property is heavily used for creating DSLs or type-safe builders.

Gotcha #2: instance vs. extension function name conflict

What happens when you create an extension function with the same signature as the regular member function? The answer is simple: unless you jump through some hoops, the member function will always win over the extension function, which is probably not what you’d want or expect.

If you’re asking yourself why would anyone do such a stupid thing, consider that you might end up in this situation because of some seemingly unrelated refactoring, or because of dependency update.

The IDE reports this as a Kotlin compilation warning, so if you’re worried about hitting this issue, it might be a good idea to clean up your project and turn on the allWarningsAsErrors flag.

Sealed classes

You’ve probably seen this kind of class:

class ResultOrError<out T>
private constructor(private val result: T?,
private val error: Throwable?) {
fun get(): T = result ?: throw error!!

companion object {
fun <T> result(result: T) = ResultOrError(result, null)
fun <T> error(error: Throwable) = ResultOrError<T>(null, error)
}
}

While this code works, it’s not the most elegant solution. The contract that either result or error is non-null is brittle, it also doesn’t scale well if you have more fields that are valid only in certain combinations.

What you really need is a way to represent that the object has a couple of completely different forms. This kind of type is most commonly known as sum type or discriminated union. In Java you might represent this type using a Visitor pattern:

abstract class ResultOrErrorVisitor<T> {
abstract T visit(Result<T> result);
abstract T visit(Error<T> error);
}

interface ResultOrError<T> {
T accept(ResultOrErrorVisitor<T> visitor);
}

class Result<T> implements ResultOrError<T> {
public final T result;

Result(T result) {
this.result = result;
}

@Override
public T accept(ResultOrErrorVisitor<T> visitor) {
return visitor.visit(this);
}
}

class Error<T> implements ResultOrError<T> {
public final Throwable error;

Error(Throwable error) {
this.error = error;
}

@Override
public T accept(ResultOrErrorVisitor<T> visitor) {
return visitor.visit(this);
}
}

In Kotlin, you can use a construct named sealed class:

sealed class ResultOrError<out T> {
data class Result<out T>(val result: T) : ResultOrError<T>()
data class Error<out T>(val error: Throwable) : ResultOrError<T>()
}

Why did this particular code pattern received a special treatment and was baked into the language? Probably because the Visitor pattern is a ton of boilerplate that’s incompatible with most peoples’ brains, and this construct is needed more often than you’d suspect.

Take a look at the List<T>.indexOf(element: T): Int method. It returns the zero-based position of the first occurrence of the specified element, or -1 if the list does not contain it.

If you think for a second this API is a bit awkward. It abuses the fact, that the return type is broader than the range of return values, so it uses an arbitrary value from the unused part of return type domain to represent the “element not found” return value.

Once you understand this case, you’ll see similar APIs all over your codebase. Fortunately, now you’ll know to refactor them to return sealed classes.

Gotcha #3: else clause in when expressions vs. sealed classes

The Visitor pattern from Java is terribly verbose, but it has one very important upside: it’s fully type safe. When you add one more Visitable implementation (i.e. you define one more form your objects can take), all your Visitor classes will have to be changed to pass the compilation.

Using when over a sealed class instance exhibits the same behavior, but only if you use it as an expression, and only if you don’t use the else clause. There is a feature request for additional syntax to enforce full type-safety, but for now, you have to rely on code reviews or custom static code analysis.

Tooling

As an Android or Java developers, you might not have given much thought to this aspect of Kotlin language. We’re hopelessly spoiled by Java tooling, so we take the same level of functionality for Kotlin language for granted.

The fact that you can just open up the IDE you’ve been using for the last couple years, hook up another programming language to the existing project, and keep all the facilities like code completion, navigation, and refactoring is simply amazing.

It’s not a coincidence. Jetbrains, the Kotlin language creators, were creating the programming tools for a long time. They know exactly the tradeoffs between the language features and the tools that can be created.

I don’t expect to get Ruby’s methodMissing or templates metaprogramming from C++ (thank goodness, generics are complex enough for me) in Kotlin. On the other hand, we can perform complex refactoring in a few keystrokes, while others are grepping like it’s 1986 or hoping their rename will include all the files. Sounds like a good deal to me 😉.

Summary

I’m not excited about Kotlin because I can channel my inner code-golfer and write everything as a single expression.

I’m not excited because of some minor syntax choices that match my preferences.

I’m excited because I’ve found a language that elegantly solves the issues I face every day. It has excellent tooling, solid feature base, and is developed by a thoughtful core team I trust to make right choices.

That’s why I’m looking forward to using the Kotlin as my primary language for the foreseeable future.

--

--