article banner

Effective Kotlin Item 20: Do not repeat common algorithms

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

I often see developers reimplementing the same algorithms again and again. By algorithms, here I mean patterns that are not project-specific, so they do not contain any business logic and can be extracted into separate modules or even libraries. These might be mathematical operations, collection processing, or any other common behavior. Sometimes these algorithms can be long and complicated, like optimized sorting algorithms. There are also many simple examples though, like number coercion in a range:

val percent = when { numberFromUser > 100 -> 100 numberFromUser < 0 -> 0 else -> numberFromUser }

Notice that we don’t need to implement this because it is already in the stdlib as the coerceIn extension function:

val percent = numberFromUser.coerceIn(0, 100)

The advantages of extracting even short but repetitive algorithms are:

  • Programming is faster because making a single call is easier than implementing an algorithm.

  • They are named, so we can recognize an algorithm by name instead of by reading its implementation. This is easier for developers who are familiar with a given algorithm. This might be harder for new developers who are not familiar with a given concept, but it pays off to learn the names of repetitive algorithms. Once we learn these names, we can benefit from that in the future.

  • We eliminate noise, making it easier to notice something atypical. In a long algorithm, it is easy to miss hidden pieces of atypical logic. Think of the difference between sortedBy and sortedByDescending. The sorting direction is clear when we call those functions, even though their bodies are nearly identical. If we needed to implement this logic every time, it would be easy to confuse whether the implemented sorting has ascending or descending order. Comments before an algorithm implementation are not helpful either. Practice shows that developers often change code without updating comments, so over time, we lose trust in comments.

  • They can be optimized once, and we profit from this optimization everywhere we use these functions.

Learn the standard library

Common algorithms have nearly always already been defined by someone else. Most libraries are just collections of common algorithms. The most special among them is the stdlib (standard library), which is a huge collection of utilities, mainly defined as extension functions. Learning the stdlib functions can be demanding, but it is worth it. Without it, developers have to reinvent the wheel time and time again. To see an example, take a look at this snippet from an open-source project:

override fun saveCallResult(item: SourceResponse) { var sourceList = ArrayList<SourceEntity>() item.sources.forEach { var sourceEntity = SourceEntity() sourceEntity.id = it.id sourceEntity.category = it.category sourceEntity.country = it.country sourceEntity.description = it.description sourceList.add(sourceEntity) } db.insertSources(sourceList) }

Using forEach here is useless. I see no advantage to using it instead of a for-loop. What I do see in this code, though, is a mapping from one type to another. We can use the map function in such cases. Another thing to note is that the way SourceEntity is set up is far from perfect. This is a JavaBean pattern that is obsolete in Kotlin; instead, we should use a factory method or a primary constructor (Chapter 5: Object creation). If, for some reason, someone needs to keep it this way, we should at least use apply to set up all the properties of a single object implicitly. This is our function after a small clean-up:

override fun saveCallResult(item: SourceResponse) { val entries = item.sources.map(Source::toEntry) db.insertSources(entries) } private fun Source.toEntry() = SourceEntity().apply { id = this.id category = this.category country = this.country description = this.description }

Implementing your own utils

At some point in every project, we need some algorithms that are not in the standard library. For instance, what if we need to calculate the product of the numbers in a collection? This is a well-known abstraction, so it is good to define it as a universal utility function:

fun Iterable<Int>.product() = fold(1) { acc, i -> acc * i }

You don’t need to wait for more than one use. A product is a well-known mathematical concept, and its name should be clear to developers. Maybe another developer will need to use it in the future, and they’ll be happy to see that it is already defined. Hopefully, that developer will find this function. It is bad practice to have duplicate functions achieving the same results. Each function needs to be tested, remembered, and maintained, all of which should be considered costs. We should not define functions we don’t need, therefore, we should first search for an existing function before implementing our own.

Notice that product, just like most functions in the Kotlin stdlib, is an extension function. There are many ways we can extract common algorithms, starting from top-level functions and property delegates and ending up with classes. However, extension functions are a really good choice because:

  • Functions do not hold states, so they are perfect for representing behavior, especially if it has no side effects.

  • Compared to other top-level functions, extension functions are better because they are suggested only on objects with concrete types.

  • It is more intuitive to modify an extension receiver than an argument.

  • Compared to companion object or static methods, extensions are easier to find among hints since they are suggested on objects. For instance "Text".isEmpty() is easier to find than TextUtils.isEmpty("Text"). This is because when you place a dot after "Text", you’ll see as suggestions all the extension functions that can be applied to this object. To find TextUtils.isEmpty, you would need to guess where it is stored, and you might need to search through alternative util objects from different libraries.

  • When we call a method, it is easy to confuse a top-level function with a method from the class or superclass, but their expected behavior is very different. Top-level extension functions do not have this problem because they must be invoked on an object.

Summary

Do not repeat common algorithms. First, it is likely that there is a stdlib function that you can use instead. This is why it is good to learn the standard library. If you need a known algorithm that is not in the stdlib, or if you need a certain algorithm often, feel free to define it in your project. A good choice is to implement it as an extension function.