In Kotlin, loops are deprecated (sort of)

Luc-Antoine Girardin
6 min readAug 13, 2021

--

Of course, Jetbrains did not deprecate Kotlin loops and never will. It would not make sense to actually deprecate them as loops have been a part of programming for decades now. There will always be cases where they can’t be avoided, and it’s important to understand how they work since everything falls back to them in the end.

That being said, while loops, for loops and do/while loops are not getting any younger. Programming langages evolve, new ones emerge, new features are included, and consequently, better alternative ways to implement algorithms come to light. Unfortunately, it’s easy to stick with those old habits without considering those evolutions.

Kotlin is still arguably a young programming language, but its standard library is so incredibly huge that one cannot remember all its content. Most of the simple algorithms loops are used for have been implemented in the form of extension functions. For example, let’s compare the following:

// 1. Old habit
val names = mutableListOf<String>()
for (user in users) {
names.add(user.name)
}
// 2. Extension
val names = users.map { it.name }

One small difference between those two implementations, is that the resulting list in the second example is immutable, which can be fixed using mapTo instead. Other than that, the algorithms are identical. The first case though is a little less concise and the intention is not clear at first glance, or maybe it is but it is a small example. Anything more complex and the intention will quickly be lost. Using map though, if the transformation complexifies, the lambda can be extracted as a function easily to keep readability.

The extension approach makes it easy to chain calls as well to apply a series of transformations for larger computations. This is the first step into functional programming, which might sound scary at first, but is just another tool to produce better code.

So here is a gentle reminder of all that can be used instead of loops to iterate through data.

Sorting

Let the premise be demonstrated with the example of sorting a collection. The fruit list will be reused in all examples as if there was nothing between each extensions and the list declaration.

val fruits = mutableListOf(
"Blueberry", "Banana", "Orange", "Apple", "Strawberry", "Cherry"
)
fruits.sort()
// [Apple, Banana, Blueberry, Cherry, Orange, Strawberry]

And that’s straightforwardly it. The collection is now sorted according to the natural ordering of String, which is alphabetically.

What if the desired order is the other way around?

fruits.sortDescending() 
// [Strawberry, Orange, Cherry, Blueberry, Banana, Apple]

What if the ordering algorithm should not consider the natural ordering, but the length of the string instead.

fruits.sortBy { it.length } 
// [Apple, Orange, Cherry, Banana, Blueberry, Strawberry]
fruits.sortByDescending { it.length }
// [Strawberry, Blueberry, Orange, Cherry, Banana, Apple]

That works nicely, but there might be complex cases where having only one element at a time does not give enough information to sort. What if the fruits should be sorted by length, but those of the same length should be sorted alphabetically?

fruits.sortWith(
compareBy<String> { it.length }.thenBy { it }
)
// [Apple, Banana, Cherry, Orange, Blueberry, Strawberry]

This is a general case, but if an even more complex sorting algorithm is required, the comparator object can be instantiated directly.

All those sorting function are well and good, but what if the original collection is not mutable, or it is but should not be altered? It might be necessary to create a new collection with the result of the sorting. Fortunately, all the sort variants have a corresponding sorted function which does exactly that.

val sorted = fruits.sorted()
val sortedDescending = fruits.sortedDescending()
val sortedBy = fruits.sortedBy { it.length }
val sortedByDescending = fruits.sortedByDescending { it.length }
val sortedWith = fruits.sortedWith(
compareBy<String> { it.length }.thenBy { it }
)

This was 10 ways to sort a collection, for different cases. Chances are they cover whatever sorting case anyone may encounter. The code is straightforward, noiseless, concise and clear.

One thing to note is that all the sort methods work only on MutableList, but their sorted counterparts work on the more general Iterable.

Other cases

If one would try to print Kotlin’s documentation on collections, which includes a description for each extensions defined in Kotlin’s standard library, on paper, it would require a total of 232 pages. Time won’t be wasted here giving examples for everything that is already explained there.

The focus of this section will be give the spotlight to the general extensions, like sort, without citing every variant. Most of the later mentioned also have few variants, but that study will be left as a homework to the reader.

Filtering

For those who want to keep only the elements that meet certain conditions.

val filtered = fruits.filter { it.length == 6 }
// [Banana, Orange, Cherry]

Similar functions include distinct and partition.

Mapping

For those who want to transform each element.

val mapped = fruits.map { it.length } // [9, 6, 6, 5, 10, 6]

Similarly, each function in the associate family will produce a Map with every Pair returned by the transformation.

Analysing

Here is a couple of extensions use to analyse the content of a collection.

val any = fruits.any { it.length == 6 } // true
val all = fruits.all { it.length == 6 } // false
val none = fruits.none { it.length == 6 } // false
val count = fruits.count { it.length == 6 } // 3
val sum = fruits.sumBy { it.length } // 42

Searching

For those searching for that particular element.

val fruit = fruits.find { it.length == 6 } // Banana

Similar extensions include first, last, minBy and maxBy.

Taking

For those who want only a certain number of elements.

val take = fruits.take(3) // [Blueberry, Banana, Orange]

The opposite would be drop, where instead of keeping the first few elements, it returns all the element but the first few.

This already covers a lot of cases, and the standard library of Kotlin contains even more. The last series of extension worth noting is probably the most important, as all the previously mentioned extensions can be rewritten using it. Introducing the holy mother of all iterating extensions…

Folding

This one work like a builder of sorts. The idea is to start with an initial value, and building onto it with each element of the collection. For example, here is an algorithm that builds a string with the first character in each string of fruits:

val totalCount = fruits.fold(initial = "") { accumulator, fruit -> 
accumulator + fruit.first()
}
// BBOASC

What happens here is, in the first iteration, accumulator == "" and fruit == "Blueberry", so the lambda returns "" + 'B'. The next iteration receives the result from the last iteration, "B", and the next fruit, "Banana", therefore the lambda returns "B" + 'B', the accumulator and the first character of "Banana". This goes on for each fruit until the result "BBOASC".

Similar functions include reduce and scan.

Final notes

One thing to keep in mind is, since a lot of extensions mentioned create more lists, to be careful when chaining those calls. An alternative would be to use sequences instead. They only execute computations if there is a terminating operation, on elements unfiltered out by the operation chain. It might be worth checking into especially for long chains or large collections.

Collections were used to demonstrate the wideness of Kotlin’s standard library, but iterating extensions are not limited to collections. Before writing any loop, a small web search might help find an existing alternative that might just be the perfect solution.

Happy coding!

--

--