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
associate
, flatten
, flatMap
, intersect
, map
, mapNotNull
, mapIndexed
, mapIndexedNotNull
,
reversed
, sorted
, sortedByDescending
, sortedWith
, union
, unzip
, zip
Filtering
binarySearch
, distinct
, distinctBy
, drop
, dropWhile
, dropLast
, dropLastWhile
,
filter
, filterIndexed
, filterIsInstance
, orEmpty
, slice
, take
, takeWhile
, takeLast
, takeLastWhile
Single element access
elementAt
, elementAtOrElse
, elementAtOrNull
, find
, findLast
, first
,
firstOrNull
, get
, getOrElse
, indexOf
, indexOfFirst
, indexOfLast
, last
,
lastIndexOf
, lastOrNull
, single
, singleOrNull
Predicates
all
, any
, contains
, containsAll
, none
, isEmpty
, isNotEmpty
Aggregators
fold
, foldIndexed
, foldRight
, foldRightIndexed
, reduce
, reduceIndexed
,
reduceRight
, reduceRightIndexed
, average
, count
, max
, maxBy
, maxOf
,
maxWith
, min
, minBy
, minOf
, minWith
, sum
, sumOf
Grouping
groupBy
, groupByTo
, groupingBy
, partition
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.