Kotlin offers a rich set of functions to operate on collections. Beyond the basics, understanding the intricacies of these functions can dramatically improve code clarity and efficiency.

Photo by Karoline Stk on Unsplash

1. Transformations with map and flatMap

Basic Syntax —

The map function transforms each element in a collection using a provided transformation function. flatMap, on the other hand, can transform and flatten collections.

val numbers = listOf(1, 2, 3)
val squared = numbers.map { it * it } // [1, 4, 9]

Real World Example —

Suppose you have a list of strings representing potential URLs, and you want to extract the domain names. Not every string is a valid URL, so this is where flatMap comes into play.

val potentialUrls = listOf(" "invalid-url", "https://another-example.com/resource")

val domains = potentialUrls.flatMap { url ->
runCatching { URL(url).host }.getOrNull()?.let { listOf(it) } ?: emptyList()
// Result: ["example.com", "another-example.com"]

2. Filtering with filter and filterNot

Basic Syntax —

filter returns a list of elements that satisfy the given predicate. filterNot does the opposite.

val numbers = listOf(1, 2, 3, 4, 5)
val evens = numbers.filterNot { it % 2 == 0 } // [1, 3, 5]

Real World Example —

Imagine filtering products not just based on a single condition, but multiple dynamic conditions (like price range, rating, and availability).

data class Product(val id: Int, val price: Double, val rating: Int, val isAvailable: Boolean)

val products = fetchProducts() // Assume this fetches a list of products

val filteredProducts = products.filter { product ->
product.price in 10.0..50.0 && product.rating >= 4 && product.isAvailable

3. Accumulation using fold and reduce

Both fold and reduce are used for accumulative operations, but they serve slightly different purposes and are used in distinct scenarios.

We will start with fold

  • Purpose — Performs an operation on elements of the collection, taking an initial accumulator value and a combining operation. It can work on collections of any type, not just numeric types.
  • Basic Syntax —
val numbers = listOf(1, 2, 3, 4)
val sumStartingFrom10 = numbers.fold(10) { acc, number -> acc + number } // Result: 20
  • Example — For instance, if you want to concatenate strings with an initial value
val words = listOf("apple", "banana", "cherry")
val concatenated = words.fold("Fruits:") { acc, word -> "$acc $word" }
// Result: "Fruits: apple banana cherry"

Let’s look at reduce

  • Purpose — Similar to fold, but it doesn’t take an initial accumulator value. It uses the first element of the collection as the initial accumulator.
  • Basic Syntax —
val numbers = listOf(1, 2, 3, 4)
val product = numbers.reduce { acc, number -> acc * number } // Result: 24
  • Example— Combining custom data structures. Consider a scenario where you want to combine ranges:
val ranges = listOf(1..5, 3..8, 6..10)
val combinedRange = ranges.reduce { acc, range -> acc.union(range) }
// Result: 1..10

Key Differences —

  1. Initial Value —
  • fold takes an explicit initial accumulator value.
  • reduce uses the first element of the collection as its initial value.

2. Applicability —

  • fold can work with collections of any size, including empty collections (because of the initial accumulator value).
  • reduce throws an exception on empty collections since there’s no initial value to begin the operation with.

3. Flexibility —

  • fold is more flexible as it allows defining an initial value that can be of a different type than the collection’s elements.
  • reduce has the type constraint that the accumulator and the collection’s elements must be of the same type.

4. Partitioning with groupBy and associateBy

Basic Syntax —

groupBy returns a map grouping elements by the results of a key selector function. associateBy returns a map where each element is a key according to the provided key selector.

val words = listOf("apple", "banana", "cherry")
val byLength = words.groupBy { it.length } // {5=[apple], 6=[banana, cherry]}

Real World Example —

data class Student(val id: String, val name: String, val course: String)

val students = fetchStudents()

// Assume students contains:
// Student("101", "Alice", "Math"), Student("101", "Eve", "History"), Student("102", "Bob", "Science")

val studentsById = students.associateBy { it.id }
// The resulting map would be:
// {"101"=Student("101", "Eve", "History"), "102"=Student("102", "Bob", "Science")}

In the above example, Eve overwrote Alice because they both have the ID “101”. The resulting map only retains the details of Eve, the last entry with that ID in the list.

Key Difference —

  • groupBy creates a Map where each key points to a List of items from the original collection.
  • associateBy creates a Map where each key points to a single item from the original collection. If there are duplicates, the last one will overwrite the others.

When deciding between the two, consider whether you need to preserve all elements with the same key (groupBy) or just the last one (associateBy).

Source link