Kotlin is often lauded for it’s conciseness and eloquent syntax, but often suffers from being confined to more limited Java coding conventions. This may be welcoming to Java developers looking to adopt Kotlin in their projects, but could be off-putting for others working in different environments, as well as, somewhat limiting Kotlin’s full linguistic potential. In this article, we’ll look at different ways to leverage the Kotlin programming language’s features and compare them with their more “Java-esque” counterparts.
TL;DR
Make use of Kotlin’s full feature set, such as, extension, operator, and infix functions, when building public facing API’s.
Builders
The Builder Pattern is a common Design Pattern for constructing objects often with complex data. It’s an approach to separating the data from the way in which the object is constructed. This allows the developer to provide the data they need to build an instance of an object, typically where defaults are used, internally in the builder, for the values that aren’t provided. Consider the following naive and hypothetical model for representing a Kotlin function’s source code:
Model
class Function<R : Any>(
val name: String,
val parameters: List<Parameter<*>> = emptyList(),
val returnType: KClass<R>,
val codeBlock: String
)
Java-esque Builder
The invocation of a Java styled builder pattern in Kotlin might look something like this:
Function.builder("sum")
.addParameter("a", Int::class)
.addParameter("b", Int::class)
.addStatement("return a + b")
.build()
As you can see, it’s a fluent API that is fairly easy to read and understand. However, some issues arise when the model becomes more complex, such as, when there are nested complex objects which also have builders. Then the API becomes difficult to visually parse and delays understanding. For instance, their could be a model to represent the source code of a Kotlin class, which can have multiple functions. This would result in nested builders in a flow, effectively convoluting the code.
Klass.builder("Math")
.addFunction(Function.builder("sum")
.addParameter("a", Int::class)
.addParameter("b", Int::class)
.addStatement("return a + b")
.build())
.addFunction(Function.builder("divide")
.addParameter("a", Int::class)
.addParameter("b", Int::class)
.addStatement("return a / b")
.build())
The more complex the data, the more illegible the code becomes. If we were to create a model representing a Kotlin source file, which takes in Klass and Function builders, then it would quickly decrease the legibility even more.
Kotlin Builder
A more Kotlin styled approach to the Builder Pattern would be to create DSLs. In Kotlin, DSLs, or Domain Specific Languages, are type-safe builders. They make use of Kotlin’s Higher-order Functions (functions that take functions as parameters) and trailing lambdas to create a nicer syntactic builder. Using this approach, the Kotlin class source code builder might look like this:
klass("Math") {
function("sum") {
+Parameter("a", Int::class)
+Parameter("b", Int::class)
+"return a + b"
}
function("divide") {
+Parameter("a", Int::class)
+Parameter("b", Int::class)
+"return a / b"
}
}
This creates a much more visually appealing API by grouping related and nested objects (and removing the trailing and accruing end parentheses on a single line). It creates a cleaner and familiar look, and at first glance is easier to ascertain.
Operator Functions
Kotlin provides the ability for Operator Overloading, which is to change the functioning of operators ( +
, -
, etc.) on different types. This feature was used in the above example with the Kotlin DSL Builder. In that example, the unary plus operator ( +item
) was overloaded to provide an alternative implementation in the context of the builder.
By providing the ability to have custom classes be interacted with using language operators, Kotlin’s operator overloading can be useful in many scenarios. For instance, consider Kotlin’s experimental Duration class. In the source code for the Duration
class, they have an overloaded operator function to provide a convenient means of adding and subtracting multiple Durations
:
public operator fun plus(other: Duration): Duration = Duration(value + other.value)
This allows the Kotlin Duration classes to be used in the following way:
val duration = 2.minutes + 30.seconds
Without this feature, getting at the same result would require either wrapping an existing plus operator, or creating a new function.
Java-esque Operator
// Explicit constructor invocation around built-in operator support
val duration = Duration(2.minutes.value + 30.seconds.value)
// Custom function
val otherDuration = 2.minutes.plus(30.seconds)
Clearly Operator Overloading provides better semantics than the above “Java-esque” approach.
Infix Functions
Another interesting feature that Kotlin supports is an Infix Function. These functions are capable of being invoked without explicitly using a period ( .
) or parentheses ( (
or )
). Let’s take a look at the to
infix function in the Kotlin standard library that creates a Pair
out of two objects.
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
The above infix function is particularly useful when constructing Maps
:
mapOf("itemOne" to "valueOne",
"itemTwo" to "valueTwo",
"itemThree" to "valueThree",
"itemFour" to "valueFour")
The invocation of infix functions reads more naturally by removing obvious glyphs and appearing more native to traditional spoken and written languages. Creative use of infix functions allows for elegant APIs. For instance, in my time library, I created an infix function called to
on the Moment
class that retrieves the Duration
difference from the first Moment
to another.
val duration = now to tomorrow
Perhaps, the function could have even been called until
, so that the function could read like so:
val duration = now until tomorrow
The possibilities are vast. However, one obvious caveat with the above infix function called to
is that it conflicts with the Kotlin standard library to
function mentioned earlier. So, you would have to be conscious of this and make sure to import the appropriate one (the compiler should alert you if you are expecting one type but get another).
Java-esque Infix Function
Java has no direct equivalent to Kotlin’s infix functions, to my knowledge. So, a function would have to be written traditionally, which in some cases is not as elegant.
val duration = now.to(tomorrow)
// Or
val otherDuration = now.until(tomorrow)
Extension Functions
Kotlin provides the concept of extension functions and properties. This gives the ability to add functions and properties, scoped to an object, that are not existing in the original object’s code. Consider the Kotlin Coroutines Flow interface, which represents a stream of data. This interface already has an extension function called map
which transforms the data in the stream from one type to another. However, it often is the case that we would have a Flow<Collection<T>>
and we need to map each value in the collection emitted by the Flow
to another value and emit the entire collection of results. Since this function isn’t already provided in the library, we would have to manually create it. Using extension functions, it might look something like this:
fun <T, R> Flow<Collection<T>>.mapEach(block: suspend (T) -> R): Flow<List<R>> =
map { collection -> collection.map { item -> block(item) } }
Which can be invoked like so:
flowOf(listOf(1, 2, 3))
.mapEach { start + it }
That looks much cleaner and is much easier than having to handle the nested map redundantly in every call site:
// Not as elegant
flowOf(listOf(1, 2, 3))
.map { numbers ->
numbers.map { start + it }
}
Java-esque Extension Functions
Once again, Java has no direct equivalent to this Kotlin feature, as far as I know, so you would have to create a function in some other class to perform the same logic. This is typically done as a “static” function (“static” is in quotes because in Java it would be a static function but in Kotlin it would be a function on a companion object).
object FlowUtils {
// Kotlin Anti-pattern
fun <T, R> mapEach(upstream: Flow<Collection<T>>, block: suspend (T) -> R): Flow<List<R>> =
// Note that the "map" function is also an extension function so if
// this wasn't provided on the class and we couldn't use extension
// functions then we would have to have a workaround for that as well
upstream.map { collection ->
collection.map { block(it) }
}
}
Which could be called like the following:
val flow = flowOf(listOf(1, 2, 3))
FlowUtils.mapEach(flow, { start + it })
It should be obvious that the Kotlin extension function approach is much nicer.
Package Level and Object Values
This one is more nuanced because the usage depends on the particular scenario, and there are subjective proponents of both methods, but just the mere fact that Kotlin provides the ability for both approachs gives it the upper hand.
Functions and properties can be on a singleton Kotlin object
or at the package level (meaning that it can be used without a static modifier or class instance). These are useful for utility functions and values. Though the more “Java-esque” approach is to use a singleton Kotlin object
which is reminiscent of Java static fields and methods.
Object Approach
object TimeUtils {
const val MILLISECONDS_IN_SECONDS = 1_000
const val SECONDS_IN_MINUTE = 60
const val MINUTES_IN_HOUR = 60
const val HOURS_IN_DAY = 24
// Wherever possible, I would advocate against having functions like these.
// In particular because it makes the ability to test and provide different
// implementations much more difficult.
fun durationFromMilliseconds(millis: Long) = millis.milliseconds
}
Where the invocations would look like:
val milliseconds = seconds * TimeUtils.MILLISECONDS_IN_SECONDS
val duration = TimeUtils.durationFromMilliseconds(1000)
Package Level Approach
const val MILLISECONDS_IN_SECONDS = 1_000
const val SECONDS_IN_MINUTE = 60
const val MINUTES_IN_HOUR = 60
const val HOURS_IN_DAY = 24
fun durationFromMilliseconds(millis: Long) = millis.milliseconds
Where the invocations would look like:
val milliseconds = seconds * MILLISECONDS_IN_SECONDS
val duration = durationFromMilliseconds(1000)
The second approach (package level) looks more elegant in the call site but opponents would argue that it pollutes the global namespace. The first approach (object) looks more verbose but groups related information together. And it should be noted that the object approach can be used similar to the package level approach depending on how it is imported, but it does add some redundancy.
import com.chrynan.example.TimeUtils.MILLISECONDS_IN_SECONDS
So which approach to go with is more nuanced and depends on the particular use case and some subjectivity.
Recapitulate
We have gone through different Kotlin features which make code more concise and readable, and have illustrated their alternatives in a more antiquated “Java-esque” style to show the contrast between the different approaches. When designing public facing APIs and libraries, take into consideration Kotlin features that may provide a better design. This will bolster the API and provide a more eloquent syntax.