I have always found learning new languages fun and Kotlin is no different. Many of us come from a Java background or at least are, to some extent, familiar with writing Java code. For me, having this previous Java experience makes learning Kotlin a bit more special, and what makes it special, is going to be the focus of this post.
I personally love discovering idiomatic ways of writing code, especially if it comes with a nicer syntax and improvements to the code quality. It has been nearly a year since my team and I, with no prior Kotlin experience, built our first two services in Kotlin. As one would expect, in this first exposure to the language, we barely took advantage of Kotlin’s way of writing code and stuck to the old-school Java style. However, with each new Kotlin service, we discovered better ways of writing what otherwise would be Java-style code.
In this post, I would like to share with you my personal pick of idioms that I like to use when working in Kotlin. I will do my best to demonstrate how a conventional (Java-ish) code can be rewritten into an idiomatic Kotlin code.
Many Kotlin idioms build on top of Kotlin's null-safety syntax, and the use of scope functions. As we’ll see, combining these two concepts makes for a nicer, more readable code, generally speaking ;). As a disclaimer, I am assuming that you, the reader, are already familiar with the fundamentals of Kotlin, especially with what makes Kotlin great - null-safety and the syntax of null-safety, such as the “safe method call” ?.
or the “elvis” operator ?:
. And with that, let's get rolling.
Just Elvis it
The “elvis” operator, ?:
, is a notation I have been using a lot and I think it is the simplest to get used to. It comes in handy when dealing with nullable types and can save you a few lines of code.
In the following code example, the function searches for an element in an HTML document. If the element is not found, the returned element is going to be null
. Since the rest of the function relies on the element, we need to guard the rest of the function logic with an if-statement that checks if the element is null
.
fun extractValue(page: Document): ValueResult? {
val divBlock: Element? = page.getElementById("id123")
if (divBlock == null) {
return null
}
...
}
The above code can be simplified with the elvis operator as follows:
fun extractValue(page: Document): ValueResult? {
val divBlock: Element = page.getElementById("id123") ?: return null
...
}
We got rid of the entire if-statement which saved us three lines of code. Since return
is an expression in Kotlin, we are able to use it alongside the elvis operator.
The elvis operator is not limited to “early” return statements. In a similar fashion, we can use it to throw an exception like so:
val divBlock: Element = page.getElementById("id123") ?: throw RuntimeException()
We can also use it to assign or return a default value:
val amlType: String = extractAmlType(text) ?: "pep"
// or
return extractAmlType(text) ?: "pep"
Run if not null
Another concept I find myself using fairly often is the let
scope function. If you’re new to scope functions I highly recommend having a look at the official documentation on Kotlin’s scope functions. In short, they are handy little functions which can be invoked on any object at hand. When calling them on your object, you define a block of code that you want to run in between curly brackets, aka a lambda function. And here’s the good part - the object you invoked the scope function on becomes available in your lambda function. This allows you to do some interesting things.
The following example demonstrates the most common use case in which I resort to using the let
scope function. We have a block of code in which we are populating a list of Strings. There are different types of values, such as name, location, political position, etc. Each value is extracted from an input string by its dedicated function which may return null
if the value cannot be extracted. In our example, the list we are populating cannot contain null
values and thus we have to check if the extracted value is not null
before adding it to the list. The conventional approach asks for an if-statement.
val extractedValues: MutableList<String> = arrayListOf()
...
val name: String? = extractName(input)
if (name != null) {
LOG.info("Extracted name: $it")
extractedValues.add(name)
}
But hold on! We can do better. Let’s bring the “safe call” operator ?.
and the let
function into the spotlight. We mix the two together and the result is the following:
val extractedValues: MutableList<String> = arrayListOf()
...
extractName(input)?.let {
LOG.info("Extracted name: $it")
extractedValues.add(it)
}
We have once again eliminated that clunky if-statement and ended up with a slicker version of the original code. At first glance, the syntax is very different from the original code, but both achieve the same result.
Since we are dealing with a nullable type object we have to use Kotlin’s “null safe” operator. This ensures that the block of code passed to the let
function is only executed if the result of extractName()
is not null
. Notice the it
argument, this is the String object returned by extractName()
. You may be wondering where the argument came from. Well, in Kotlin, it
is the implicit name for the lambda’s parameter and does not have to be explicitly declared. This however only applies to single-parameter lambda functions.
The takeIf method
The takeIf()
function is similar to scope functions, although it is not marked as such in the official documentation. To understand its usefulness, let’s have a look at a code example where it can be used.
In the below code snippet, we are constructing a data object, ResponseBody
, in which one of its fields is a list of Strings. The object is then later automatically serialised into a JSON String. Since we don’t want to assign an empty list to the values
field, we check if the list of values being assigned is empty and if it is, we assign null
instead.
val validValues: List<String> = validate(values)
return ResponseBody(
...
values = if (validValues.isNotEmpty()) validValues else null
...
)
Now, let’s have a look at how we can replace the unwieldy if statement with the takeIf()
function.
As with a scope function, we provide takeIf()
a lambda body. This time, the lambda serves as a predicate so we need to make sure that it returns a boolean, i.e. true
or false
. We return true
if we want to assign the object at hand, and false
if we want to reject the object and assign null
instead.
return ResponseBody(
...
values = validate(values).takeIf { it.isNotEmpty() }
...
)
I personally find the version of code that uses takeIf()
much more readable.
There is another function, called takeUnless()
, that does the exact opposite of takeIf()
. It returns null
if the predicate evaluates to true
, otherwise, it returns the object.
String prefix and suffix
I think it is safe to assume that most of us have had to extract a prefix or a suffix from a String at some point in our career. Your preferred way of solving this common problem may involve finding the index of a delimiter and passing it to a “substring” method of some kind called the input String. Or maybe you opt-in for a much slicker approach, albeit less efficient, like the one in the following example:
return if (text.contains(":")) {
text.split(":").first()
} else {
""
}
The above code, even though already easier to read than its “index-based” equivalent, still comes short of what Kotlin provides out of the box. The code can be reduced to a single line with the extension function substringBefore()
like so:
return text.substringBefore(":", missingDelimiterValue = "")
Similarly, as substringBefore()
can be used for prefixes, the substringAfter()
extension function can be used for suffixes. If the delimiter value passed to these two functions is not present, the original String is returned by default.
Building a list
We have reached the last idiom on this list which, funnily enough, is about building an actual “list” in Kotlin by using the buildList()
function. Personally, I like to use it when certain conditions are met which might be why I have used it only sparingly. Let me describe a scenario that I find buildList()
useful.
Consider the following code example in which we are populating a list of non-null String values that we want to return at the end. There are different types of String values that we are adding to the list, such as name, occupation and relatives. We are extracting these values from an HTML page and each value type has its dedicated function that does the extraction. However, there is some inconsistency in the return type of these functions. One returns a nullable String and the other a list of Strings.
fun extractValuesFromPage(page: Document): List<String> {
val name: String = extratName(page)
val occupation: String? = extractOccupation(page)
val relatives: List<String> = extractRelatives(page)
val results: MutableList<String> = arrayListOf()
results.add(name)
if (occupation != null) {
results.add(occupation)
}
results.addAll(relatives)
return results
}
The above code works, but it is not as readable as I would like it to be. This is where the buildList()
function comes into play. It looks similar to the scope functions we have seen so far. We define the body of a lambda in which the scope is in the context of a mutable list. This means we are able to call the member functions of the list such as add()
or addAll()
in our lambda body. The result looks like this:
fun extractValuesFromPage(page: Document): List<String> {
val name: Result = extratName(page)
val occupation: Result? = extractOccupation(page)
val relatives: List<Result> = extractRelatives(page)
return buildList {
add(name)
if (occupation != null) {
add(occupation)
}
addAll(relatives)
}
}
In my opinion, this looks tidier and easier to read.
Similar to the buildList()
function, Kotlin provides its equivalent for building a set (buildSet()
) and a map (buildMap()
).
To wrap it up
These five coding “techniques” I have shared with you are only a fraction of what Kotlin has to offer. Kotlin has an entire page dedicated to idioms some of which are included in this post. If you were not keen on Kotlin or are just starting out I hope this read has sparked your interest in the language.