🚗 · Collection Operations: Overview

5 min read · Updated on by

Read on for a list of the collection transformations you should definitely know about, and an overview of patterns in the names of operations that you will encounter.

In the previous article, we talked about the usefulness of both the object-oriented and functional styles, and explained when you should use which:

Structure your programs using OOP, and implement behaviors using FP.

Of course, to be able to do implement behaviors using FP, you have to learn the names and purposes of the FP building blocks, in the same way you learned OOP patterns — builders, factories etc. The Kotlin Standard Library is chock-full with them, and it is the purpose of the following set of articles to give you a passing familiarity with those that are used most often.

However, unless you’re already familiar with this style of thinking, it will probably be more that you can memorize, so instead, I recommend this: focus on designing behaviors in the same way you design programs. Think before you write, decompose your behaviors into atomic pieces, and then search the internet to see if the standard library already contains something similar. Over time, you’ll absorb the contents of the standard library and learn to get a feeling for which patterns are “core” and almost certainly implemented vs. which ones aren’t, and need to be implemented locally.

Collection operations

To give you an idea of the breadth covered by the standard library, here is a (partial) list of available functions. We will be covering a large part of them in the following articles. All of them are defined as extension functions.

See if you can figure out what they do just based on the name.

Transformers

associateflattenflatMapintersectmapmapNotNullmapIndexedmapIndexedNotNull

reversedsortedsortedByDescendingsortedWithunionunzipzip

Filtering

binarySearchdistinctdistinctBydropdropWhiledropLastdropLastWhile,

filterfilterIndexedfilterIsInstanceorEmptyslicetaketakeWhiletakeLasttakeLastWhile

Single element access

elementAtelementAtOrElseelementAtOrNullfindfindLastfirst

firstOrNullgetgetOrElseindexOfindexOfFirstindexOfLastlast

lastIndexOflastOrNullsinglesingleOrNull

Predicates

allanycontainscontainsAllnoneisEmptyisNotEmpty

Aggregators

foldfoldIndexedfoldRightfoldRightIndexedreducereduceIndexed

reduceRightreduceRightIndexedaveragecountmaxmaxBymaxOf

maxWithminminBy, minOfminWithsumsumOf

Grouping

groupBygroupByTogroupingBypartition

Naming patterns

There are a few patterns in the way collection operations are named that are useful to be aware of. For a operation {o} (e.g. map), usually at least some of the following are defined:

{o}To (e.g. mapTo)

Always accepts a mutable collection as one of its parameters. Instead of returning a new collection, as the result of the transformation, instead inserts the result into the mutable collection that was passed.


//sampleStart
fun main() {
    val list = listOf(1, 2, 3)
    val result = mutableListOf<Int>()

    println(list.map { it + 1}) // listOf(2, 3, 4)
    println(result) // mutableListOf()

    println(list.mapTo(result) { it + 1 }) // Unit
    println(result) //mutableListOf(2, 3, 4)
}
//sampleEnd

{o}By (e.g. maxBy)

Instead of applying the operation to the collection directly, it first applies a transformation to each element. For example, max finds the maximum value in a collection, and therefore naturally requires the collection to contain comparable elements. Given that, think about how you would go about finding the oldest person in a collection of people. You would have to extract the ages, construct some mapping between them and the person they correspond to, find the maximum age, and then extract the corresponding person. What a mess. This is exactly what the {o}By variants are for:


//sampleStart
interface Person {
    val age: Int
}

fun List<Person>.oldest() = maxBy { it.age }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the composer in the code's symphony,
        With delegates and lambdas, pure harmony.
        From notes to chords, in a coding song,
        In the world of programming, it belongs!
    """.trimIndent()
    println(poem)
}

From time to time, you will also encounter {o}With variants. They are similar to {o}By in that they allow you to give additional instructions on how the operation should be performed, but differently — e.g. maxWith allows you to pass in a Comparator that will be used to find the maximum.

The only exception to this rule I’m aware of is associate, where associateBy and associateWith don’t conform to this pattern (and instead carry a much more natural meaning). We’ll talk about them in the chapter on transformations.

{o}Indexed (e.g. mapIndexed)

Passes an additional parameter to the operation containing the index of the element that is being operated on:


//sampleStart
fun main() {
    val result = listOf("a", "b", "c")
        .mapIndexed { idx, letter -> "$idx $letter"}

    println(result) // listOf("0 a", "1 b", "2 c")
}
//sampleEnd

{o}orNull (e.g. firstOrNull)

Used with operations that might not have a defined result — e.g. for first, there is no sensible result when the list is empty. The regular variant throws an exception, while the {o}orNull variant returns null instead.


//sampleStart
emptyList<Any>().first() // Throws NoSuchElementException
emptyList<Any>().firstOrNull() // null
//sampleEnd

Imperative verbs vs. adjectives

Most operations are defined on immutable collections, and therefore do not modify the original collection and return a new collection — e.g. filter doesn’t not remove elements from the collection it is run on.

However, with mutable collections, there are certain operations that can also be performed in-place, e.g. sorting. To differentiate between the two, the form of an imperative verb (e.g. sort) is used for the variant that modifies the collection in place, while the form of an adjective (e.g. sorted) is used for the variant that returns a new collection.

More information can be found in the docs.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer