Write fluent code in Kotlin

syIsTyping
Level Up Coding
Published in
12 min readMay 27, 2020

--

Photo by Mike Lewis HeadSmart Media on Unsplash

Have you ever read a piece of code and thought “I’m not reading code, I’m reading a story!”. Fluent code reads like prose; it tells a story of what the author is trying to achieve that reads like plain English. The code just flows; it takes next to no effort to read. Like beautiful prose, fluent code takes effort to create. When you read a piece of code that just reads itself, the author has taken that effort on your behalf to shape it.

I subscribe to the “make it work, make it beautiful, make it fast” mentality, and a component of beautiful code is its fluency. While writing and reviewing code, I often find myself frequently using a few “style choices” to achieve readable code. However, the readability of a piece of code is subjective and more art than science, so your mileage may vary depending on your preferences and the conventions in your environment.

Disclaimer: The suggestions below are of my opinion and may not be a generally-accepted best practice.

Use comments sparingly, if any at all

Comments are not part of the code. They are ignored by the compiler and exist only for the coders. They are also another point of maintenance and can grow outdated as the code evolves separately.

Uncle Bob’s “Clean Code” book sums it up nicely by the phrase “explain yourself in code”. We should document our intention in the code itself, by using clear naming and other structures mentioned below that make the code readable. If we think the code needs some explanation, instead of writing a comment, try to refactor to encode the meaning into the code itself.

Think back to the last comment you’ve read. Was it useful? Was it accurate? Could the code be changed to incorporate that information?

There is an exception though: if we are writing a library that others will use, adding comments to the public-facing APIs, with the appropriate convention, could be useful for documentation generation.

Use Exceptions instead to signal abnormal behaviour

If we want to signal abnormal behaviour, throw an Exception instead of returning an error value, for the following reasons:

  • Abnormal behaviour doesn’t get silently passed over, instead it will propagate until the first error handler, which is usually what we want
  • The caller doesn’t need to “remember” to check for error scenarios, which means fewer error handling boilerplate
  • Many Kotlin standard library constructs play nicely with Exception-oriented signalling (eg, Result, Preconditions)
  • Kotlin doesn’t enforce declaring/handling checked exceptions like Java does, so the usual boilerplate doesn’t exist in Kotlin.

For signalling normal program behaviour, don’t use Exceptions. Instead we have 2 options:

  • To distinguish between process statuses, use Sealed Classes which works well with the when construct (more on that below)
  • To handle expected failures (such as user input validation), use null return values (for eg, using Kotlin’s many toFooOrNull() methods)

Use require/check for validating conditions that should not happen

Kotlin has a set of precondition validation functions (in a file helpfully called Preconditions.kt). Instead of an if condition, using require or check adds meaning to the code and reduces verbosity.

  • require if we’re checking an input or argument. This throws IllegalArgumentException.
  • check for other scenarios. This throws IllegalStateException.

If we’re using a Kotlin nullable object or a platform type and don’t expect it to be null at this point, use either requireNotNull or checkNotNull.

require(arg.length < 10) {
"message"
}

val result = checkNotNull(bar(arg)) {
"message"
}

/////////////// instead of ///////////////

if (arg.length < 10) {
throw IllegalArgumentException("message")
}

val result = bar(arg)
?: throw IllegalStateException("message")

Use extension functions to add meaning and enable chaining

If we have a code chunk that needs a comment, that code chunk should become a method, like this (but read on; there’s more):

val user = getUser(id)
validate(user)
activate(user)

private fun validate(user: User) {
// validate
}

private fun activate(user: User) {
// activate
}

/////////////// instead of ///////////////

val user = getUser(id)

/*
* Validate user
*/
// validate

/*
* Activate user
*/
// activate

When we create methods like above, we’ve encapsulated some meaning but we can go further to make it more fluent. A big part of fluent code is the designing fluent interfaces, and a big part of that is the ability to chain relevant operations together. If we control the User class, we could add validate and activate as methods, but if both methods only make sense in this class (or if we don’t own User), extension functions come to the rescue.

In fact, it’s recommended to use extensions as much as possible!

Use extension functions liberally. Every time you have a function that works primarily on an object, consider making it an extension function accepting that object as a receiver.

private fun User.validate(): User { 
// validate
return this
}

private fun User.activate(): User {
// activate
return this
}

...

val user = getUser(id)
.validate()
.activate()

Consider using or creating infix functions to reduce verbosity

There are many infix functions in the Kotlin standard library and they serve to reduce nested parentheses.

val x = mapOf(1 to "a")
val range = 1 until 10
val loop = listOf(...) zip listOf(...)

/////////////// instead of ///////////////

val x = mapOf(1.to("a"))
val range = 1.until(10)
val loop = listOf(...).zip(listOf(...))

We could create our own infix functions, and I would recommend it only if the function fulfils the following criteria:

  • no side effects
  • simple logic
  • short name
  • used in a place that can benefit from fewer parentheses

The last point is important, and presents a scenario not to use infix functions: infix functions might not chained in a readable way due to the lack of parentheses. For example, if we have 2 functions which we use in a chain:

private fun process(arg: String) = // some string
private fun String.foo(x: String) = // some string

val bar = process("a")
.foo("b")
.min()

If foo was infix, using it would look pretty weird since additional parentheses are needed to get the expected behaviour. In this case, it is ok not making it infix due to the way it is used.

val bar = (process("a") foo "b").min()  // bad

Use scope functions to reduce verbosity

with helps to create a section of logic that relate to an object.

...some code...
with(foo.id) {
LOGGER.info("id is $this")
doSomething() // method of id
doSomethingElse(this)
}
...some code...

/////////////// instead of ///////////////

...some code...
val id = foo.id
LOGGER.info("id is $id")
id.doSomething()
doSomethingElse(id)
...some code...k

apply works wonders when interacting with objects with only setters.

val foo = Foo().apply {
field1 = 1
field2 = "a"
}

/////////////// instead of ///////////////

val foo = Foo()
foo.field1 = 1
foo.field2 = "a"

also can let us reuse an object to add additional effects

requireNotNull(foo) {
"message with ${foo.id}"
.also { LOGGER.error(it) }
}

/////////////// instead of ///////////////

requireNotNull(foo) {
val message = "message with ${foo.id}"
LOGGER.error(message)
message
}

/////////////// or worse ///////////////

requireNotNull(foo) {
LOGGER.error("message with ${foo.id}")
"message with ${foo.id}"
}

I’ve written a quick reference to the scope functions.

Omit type information as a default

Kotlin has type inference and the IDE shows type information as hints, so we almost always want to omit writing it in code, except when mandated by syntax.

val x = "a"

override fun foo() = 1

/////////////// instead of ///////////////

val x: String = "a"

override fun foo(): Int = 1

There are however, a few scenarios where type information would be useful:

  • When the return type of a function or field is too complicated to determine with a glance, egMap<Int, Map<String, String>>
  • When returning a platform type.

Use expression syntax if the function has 1 expression

As long as the function consist of one expression, no matter how long, we should prefer using expression syntax.

fun foo(id: Int) = getFoo(id)
.chain1()
.chain2()
.chain3()
.chain4 {
// some lambda
}

/////////////// instead of ///////////////

fun foo(id: Int): Bar {
return getFoo(id)
.chain1()
.chain2()
.chain3()
.chain4 {
// some lambda
}
}

This works very well in combination with scope functions.

fun foo(arg: Int) = Foo().apply {
field1 = arg
field2 = "a"
field3 = true
}

/////////////// instead of ///////////////

fun foo(arg: Int): Foo {
return Foo().apply {
field1 = arg
field2 = "a"
field3 = true
}
}

Exception: when the return type is Unit, don’t use expression syntax even if the method called also returns Unit or void. This makes it easier to see at a glance that the function doesn’t return anything.

fun foo() {
barThatReturnsUnitOrVoid()
}

/////////////// instead of ///////////////

fun foo() = barThatReturnsUnitOrVoid()

Use typealias or inline classes to add meaning to common types

Some types are generic or a mouthful (eyeful?). We can use either typealias or inline classes to add encode some meaning to the otherwise difficult-to-understand types.

typealias CustomerId = Int
typealias PurchaseId = String
typealias StoreName = String
typealias Report = Map<CustomerId, Map<PurchaseId, StoreName>>

fun(report: Report) = // ...

/////////////// instead of ///////////////

fun(report: Map<Int, Map<String, String>>) = // ...

Inline classes are similar except we declare an actual class to wrap the original type. The benefit over typealias is that whereas the CustomerId typealias accepts any Int, a CustomerId inline class will only accept other CustomerId.

Use precision tags to specify precision of numeric literals

Kotlin has precision tags to distinguish between literals of Double/Float and Int/Long. Prefer them to specifying the type.

val x = 1L
val y = 1.2f

/////////////// instead of ///////////////

val x: Long = 1
val y: Float = 1.2

Use underscores to visually group numeric literals

Use underscores whenever possible in literals.

val x = 1_000_000

/////////////// instead of ///////////////

val x = 1000000

Use string templates and raw strings

String templates (or string interpolation) is preferred over concatenation, String.format, or MessageFormat in most cases. Raw strings are also useful when working with multi-line text or text with lots of special chars that would otherwise need escaping.

val x = "customer $id bought ${purchases.count()} items"
val y = """He said "I'm tired""""

/////////////// instead of ///////////////

val x = "customer " + id + " bought " + purchases.count() + " items"
val y = "He said \"I'm tired\""

Use Elvis Operator to return if null

A common scenario when handling nullable types is returning some value if nulls are expected, in which case we might use the if-null-then-return pattern. The elvis operator is handy in this case.

val user = getUser() 
?: return 0

/////////////// instead of ///////////////

val user = getUser()
if (user == null) {
return 0
}

However, what if user is from a method argument? There is no getUser() to attach?:. For consistency we should still prefer using ?: over if.

fun foo(user: User?): Int {
user ?: return 0
// ...
}

/////////////// instead of ///////////////

fun foo(user: User?): Int {
if(user == null) {
return 0
}
// ...
}

Use Stream and Sequence correctly

When coming from Java, we may habitually use .stream() when transforming collections. If we do that, we would actually be using the Java Stream API. Kotlin has its stream methods defined on Iterable instead.

listOf(1).map { ... }

/////////////// instead of ///////////////

listOf(1).stream().map { ... }.collect(...)

Kotlin’s “stream” methods are eager while Java’s are lazy, so the equivalent would be Kotlin’s Sequence methods. For handling large collections or multi-step transformations, using the Sequence methods will result in better performance without much sacrifice to readability.

listOf(1).asSequence()
.filter { ... }
.map { ... }
.maxBy { ... }

/////////////// instead of ///////////////

listOf(1)
.filter { ... }
.map { ... }
.maxBy { ... }

Use Aspect-Oriented pattern to attach side effects

I’ve elaborated more in this article, but in general, this pattern lets us add side effects without much verbosity. As a quick example, if we have a function like this:

fun getAddress(
customer: Customer
): String {
return customer.address
}

We could easily add caching by defining a cachedBy function using the pattern and changing just a small part of code:

fun getAddress(
customer: Customer
): String = cachedBy(customer.id) {
customer.address
}

/////////////// instead of ///////////////

fun getAddress(
customer: Customer
): String {
val cachedValue = cache.get(customer.id)
if (cachedValue != null) {
return cachedValue
}

val address = customer.address
cache.put(customer.id, address)
return address
}

Use Sealed Classes to handle process statuses

I’ve elaborated more in this article, but in general sealed classes lets us reduce verbosity in evaluating status codes and avoid maintaining additional status enums.

Use backticks for test method names

If we’re writing verbose method test names, it’s often useful to use backticks to make it easier to read. This will let us use spaces and some special characters.

fun `test foo - when foo increases by 3% - returns true`() { ... }

/////////////// instead of ///////////////

fun testFoo_whenFooIncreasesBy3Percent_returnsTrue() { ... }

Prefer breadth-heavy instead of depth-heavy code

This is not Kotlin-specific but we’ll explore how Kotlin makes it easier.

Depth-heavy code is code that has many “layers”, or calls to unfamiliar^ methods. This is not a technical but a cognitive concern. In general, we have limited working memory, and when reading code, each call to another unfamiliar method adds more context to working memory, which increases cognitive load.

This is best explained with an example. Say we have these fun :

fun foo(): Foo {
val foo = getFoo()
return foo1(foo)
}

private fun foo1(foo: Foo): Foo {
...something with foo...
return foo2(foo)
}

private fun foo2(foo: Foo): Foo {
...something with foo...
return foo3(foo)
}

private fun foo3(foo: Foo): Foo {
...something with foo...
return foo
}

When we read a call to foo(), we see a call to foo1(). Now we put foo() to the back of our working memory “stack” and read foo1(). So on and on until we reach foo3(), then we start walking back to foo(). This means that to understand foo(), we need to add many “items” in our working memory, each item being the context of the function that is called.

Compare this to a breadth-heavy equivalent:

fun foo(): Foo = 
getFoo()
.foo1()
.foo2()
.foo3()

private fun Foo.foo1(): Int {
...something with this (which is foo)...
return this
}

private fun Foo.foo2(): Foo {
...something with this (which is foo)...
return this
}

private fun Foo.foo3(): Foo {
...something with this (which is foo)...
return this
}

With the second approach, we only need to keep 1 context in our “stack” — that of foo(). Each time another fun is invoked, it returns back to the same context so we don’t have to “go deeper” so to speak.

As another example, this line has many nested layers:

return Foo(
bar.doSomething(
getId(SomeEnum.ENUM_1),
"string"
)
)

With some extension functions and scope functions, this can be “un-nested” like so:

return SomeEnum.ENUM_1
.getId()
.let {
bar.doSomething(it, "string")
}.let {
Foo(it)
}

The examples are exaggerated and this rule-of-thumb doesn’t work for all scenarios. However, a breadth-heavy approach would be easier to read in most cases. This can also be achieved in Java but Kotlin makes it a bit easier due to some constructs such as extension functions and scope functions.

In summary, Kotlin provides a few syntactic features that helps in “un-nesting”. When working with code that has many layers, consider experimenting with constructs to reduce the layers and choose the approach that is more readable to our audience.

^A note about “unfamiliar” methods: these are methods that the reader is not expected to recognise at a glance, so it excludes most “built-in” methods such as String.split as well as commonly-used methods in our code that could be complex but instantly understood by our readers.

Isolate non-fluent code

Sometimes no matter how hard we try, a chunk of code may still look unreadable because:

  • it has awkward operations or uses unwieldy APIs that none of the suggestions above can tame
  • there are performance considerations that dictate the structure of the code
  • we just don’t have the time to invest in crafting it at the moment

The best thing to do at this point is to isolate the code, so that other parts of the codebase do not have to interact with it directly. Regardles of whether we use a standalone class or a separate method, spend some time to craft readable and meaningful method signatures.

Isolating the code often also makes the code more testable, which is vital when the underlying code is not-so-readable as it serves as a living documentation of the expected behaviour.

... some fluent code ...
val fee = activateCustomerAndCalculateFee(
userId,
FeeType.SIMPLE,
DEFAULT_CUSTOMER_TYPE
)
... other fluent code ...

fun calculateFeeForCustomer(
userId: String,
feeType: FeeType,
customerType: Int
): Double {
... some complicated or not-so-readable code
}

In general, if we are stuck between multiple ways of writing some code, I use these rules of thumb to make the choice. Choose the option that:

  • Makes the intention of the code clear. Future-you would thank yourself!
  • Optimises skimming. Choose code constructs that make it easy for the reader to skim to the right spot in the code.
  • Is less verbose. The lesser is written the lesser to be distracted by.

You’ve reached the end! This is a living article and will grow as I come across more “style choices”, so come back once in a while to check for updates!

References

--

--

Security Engineer in Japan. I've learnt a lot from the community, so I hope to contribute back. I write (hopefully useful) technical articles and how-to guides.