# Operations on Collections

An essential aspect of functional languages is the ability to easily perform batch operations on collections of objects.

Most functional languages provide powerful support for working with collections, and Kotlin is no exception. You’ve already seen map(), filter(), any() and forEach(). This atom shows additional operations available for Lists and other collections.

We start by looking at various ways to manufacture Lists. Here, we initialize Lists using lambdas:

// OperationsOnCollections/CreatingLists.kt
import atomictest.eq

fun main() {
  // The lambda argument is the element index:
  val list1 = List(10) { it }
  list1 eq "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"

  // A list of a single value:
  val list2 = List(10) { 0 }
  list2 eq "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]"

  // A list of letters:
  val list3 = List(10) { 'a' + it }
  list3 eq "[a, b, c, d, e, f, g, h, i, j]"

  // Cycle through a sequence:
  val list4 = List(10) { list3[it % 3] }
  list4 eq "[a, b, c, a, b, c, a, b, c, a]"

This version of the List constructor has two parameters: the size of the List and a lambda that initializes each List element (the element index is passed in as the it argument). Remember that if a lambda is the last argument, it can be separated from the argument list.

MutableLists can be initialized in the same way. Here we see the initialization lambda both inside the argument list (mutableList1) and separated from the argument list (mutableList2):

// OperationsOnCollections/ListInit.kt
import atomictest.eq

fun main() {
  val mutableList1 =
    MutableList(5, { 10 * (it + 1) })
  mutableList1 eq "[10, 20, 30, 40, 50]"
  val mutableList2 =
    MutableList(5) { 10 * (it + 1) }
  mutableList2 eq "[10, 20, 30, 40, 50]"

Note that List() and MutableList() are not constructors, but functions. Their names intentionally begin with an upper-case letter to make them look like constructors.

Many collection functions take a predicate and test it against the elements of a collection, some of which we’ve already seen:

  • filter() produces a list containing all elements matching the given predicate.
  • any() returns true if at least one element matches the predicate.
  • all() checks whether all elements match the predicate.
  • none() checks that no elements match the predicate.
  • find() and firstOrNull() both return the first element matching the predicate, or null if no such element was found.
  • lastOrNull() returns the last element matching the predicate, or null.
  • count() returns the number of elements matching the predicate.

Here are simple examples for each function:

// OperationsOnCollections/Predicates.kt
import atomictest.eq

fun main() {
  val list = listOf(-3, -1, 5, 7, 10)

  list.filter { it > 0 } eq listOf(5, 7, 10)
  list.count { it > 0 } eq 3

  list.find { it > 0 } eq 5
  list.firstOrNull { it > 0 } eq 5
  list.lastOrNull { it < 0 } eq -1

  list.any { it > 0 } eq true
  list.any { it != 0 } eq true

  list.all { it > 0 } eq false
  list.all { it != 0 } eq true

  list.none { it > 0 } eq false
  list.none { it == 0 } eq true

filter() and count() apply the predicate against each element, while any() or find() stop when the first matching result is found. For example, if the first element satisfies the predicate, any() returns true right away, while find() returns the first matching element. The only time all the elements are processed is if the list contains no elements matching the given predicate.

filter() returns a group of elements satisfying the given predicate. Sometimes you may be interested in the remaining group—the elements that don’t satisfy the predicate. filterNot() produces this remaining group, but partition() can be more useful because it simultaneously produces both lists:

// OperationsOnCollections/Partition.kt
import atomictest.eq

fun main() {
  val list = listOf(-3, -1, 5, 7, 10)
  val isPositive = { i: Int -> i > 0 }

  list.filter(isPositive) eq "[5, 7, 10]"
  list.filterNot(isPositive) eq "[-3, -1]"

  val (pos, neg) = list.partition { it > 0 }
  pos eq "[5, 7, 10]"
  neg eq "[-3, -1]"

partition() produces a Pair object containing Lists. Using [Destructuring Declarations](javascript:void(0)), you can assign the elements of the Pair to a parenthesized group of vars or vals. Destructuring means defining multiple vars or vals and initializing them simultaneously, from the expression on the right side of the assignment. Here, destructuring is used with a custom function:

// OperationsOnCollections/PairOfLists.kt
package operationsoncollections
import atomictest.eq

fun createPair() = Pair(1, "one")

fun main() {
  val (i, s) = createPair()
  i eq 1
  s eq "one"

filterNotNull() produces a new List with the nulls removed:

// OperationsOnCollections/FilterNotNull.kt
import atomictest.eq

fun main() {
  val list = listOf(1, 2, null)
  list.filterNotNull() eq "[1, 2]"

In [Lists](javascript:void(0)), we saw functions such as sum() or sorted() applied to a list of comparable elements. These functions can’t be called on lists of non-summable or non-comparable elements, but they have counterparts named sumBy() and sortedBy(). You pass a function (often a lambda) as an argument, which specifies the attribute to use for the operation:

// OperationsOnCollections/ByOperations.kt
package operationsoncollections
import atomictest.eq

data class Product(
  val description: String,
  val price: Double

fun main() {
  val products = listOf(
    Product("bread", 2.0),
    Product("wine", 5.0)
  products.sumByDouble { it.price } eq 7.0

  products.sortedByDescending { it.price } eq
    "[Product(description=wine, price=5.0)," +
    " Product(description=bread, price=2.0)]"
  products.minByOrNull { it.price } eq
    Product("bread", 2.0)

Note that we have two functions sumBy() and sumByDouble() to sum integer and double values, respectively. sorted() and sortedBy() sort the collection in ascending order, while sortedDescending() and sortedByDescending() sort the collection in descending order.

minByOrNull returns a minimum value based on a given criteria or null if the list is empty.

take() and drop() produce or remove (respectively) the first element, while takeLast() and dropLast() produce or remove the last element. These have counterparts that accept a predicate specifying the elements to take or drop:

// OperationsOnCollections/TakeOrDrop.kt
import atomictest.eq

fun main() {
  val list = listOf('a', 'b', 'c', 'X', 'Z')
  list.takeLast(3) eq "[c, X, Z]"
  list.takeLastWhile { it.isUpperCase() } eq
    "[X, Z]"
  list.drop(1) eq "[b, c, X, Z]"
  list.dropWhile { it.isLowerCase() } eq
    "[X, Z]"

Operations like those you’ve seen for Lists are also available for Sets:

// OperationsOnCollections/SetOperations.kt
import atomictest.eq

fun main() {
  val set = setOf("a", "ab", "ac")
  set.maxByOrNull { it.length }?.length eq 2
  set.filter {
  } eq listOf("ab")
  set.map { it.length } eq listOf(1, 2, 2)

maxByOrNull() returns null if a collection is empty, so its result is nullable.

Note that filter() and map(), when applied to a Set, return their results in a List.

Exercises and solutions can be found at www.AtomicKotlin.com.