πŸš— Β· Transformations

9 min read Β· Updated on by

Read on for an introduction to the most important collection transformations β€” map, flatMap, flatten, intersection, union, subtract, sorted, reverse, associate, zip, unzip, and their variants.

Element-wise transformations

map


//sampleStart
inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>
//sampleEnd

The most fundamental collection transformation, and the most often used, is map, which accepts a lambda and applies it to every element of a collection.

Example


//sampleStart
fun main() {
    val myCollection = listOf(1, 2, 3)
    myCollection.map { it + 1 }.also(::println) // listOf(2, 3, 4)
}
//sampleEnd

This use-case is so ubiquitous that you would be hard pressed to find any piece of code where map is not used.

There are a few useful variants of map:

One is mapNotNull, which also applies a transformation, but only includes the result if it’s non-null.


//sampleStart
inline fun <T, R : Any> Iterable<T>.mapNotNull(
    transform: (T) -> R?
): List<R>
//sampleEnd

Example


//sampleStart
interface FoodOrder {
    var note: String?
}
val List<FoodOrder>.notes get() = mapNotNull { it.note }
//sampleEnd
fun main() {
    val poem = """
        When you're sailing in the sea of code,
        Kotlin's syntax is the compass, the road.
        With waves and currents, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

Another variant is mapIndexed, which also passes the index of the element to the lambda:


//sampleStart
inline fun <T, R> Iterable<T>.mapIndexed(
    transform: (Int, T) -> R
): List<R>
//sampleEnd

Example


//sampleStart
interface Person

data class RacePlacement(val runner: Person, val placement: UInt) {
    override fun toString(): String = "$runner finished $placement${ordinalSuffix(placement)}"

    private fun ordinalSuffix(num: UInt) = when(num) {
        1u -> "st"
        in 2u..3u -> "nd"
        else -> "th"
    }
}

fun buildRacePlacements(racersOrderedByPlacement: List<Person>) = 
    racersOrderedByPlacement.mapIndexed { placement, person -> 
        RacePlacement(person, placement.inc().toUInt()) 
    }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's tower,
        With sealed classes, it builds the power.
        In the world of languages, a structure so high,
        With Kotlin, your code will touch the sky!
    """.trimIndent()
    println(poem)
}

Finally, we have mapIndexedNotNull, which is a combination of the previous two.


//sampleStart
inline fun <T, R : Any> Iterable<T>.mapIndexedNotNull(
    transform: (Int, T) -> R?
): List<R>
//sampleEnd

All the above have *To variants, which perform the same operation, but insert the result into a passed-in mutable collection:


//sampleStart
fun main() {
    val result: MutableList<String> = mutableListOf()
    val firstList = listOf("one", "two", "three")
    val secondList = listOf(1, 2, 3)

    firstList.mapIndexedTo(result) { idx, word -> "${idx + 1} is $word" }
    secondList.mapTo(result) { num -> "${num * 2}" }

    println(result) //[1 is one, 2 is two, 3 is three, 2, 4, 6]
}
//sampleEnd

flatMap

A related function to map is flatMap, which also performs a transformation of each element, but expects the transformation to produce collections of elements, which then get β€œflattened” (the 'flat' in flatMap) or β€œmerged” into a single List.


//sampleStart
inline fun <T, R> Iterable<T>.flatMap(
    transform: (T) -> Iterable<R>
): List<R>
//sampleEnd

Example


//sampleStart
interface Sale

interface Product {
    val sales: List<Sale>
}

// This wouldn't work, because the result would be List<List<Sale>> !
// fun getSalesOfProducts(products: List<Product>) = products.map { it.sales }

// This works
fun getSalesOfProducts(products: List<Product>) = products.flatMap { it.sales }
//sampleEnd
fun main() {
    val poem = """
        In the code's carnival, Kotlin's the ride,
        With extension functions, it's the guide.
        From loops to spins, a coding spree,
        In the world of development, it's the key!
    """.trimIndent()
    println(poem)
}

To get a feel for the relationship between map and flatMap, it can be useful to realize that you can define map in terms of flatMap:


//sampleStart
inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> = flatMap { 
    listOf(transform(it)) 
}
//sampleEnd

However, you cannot do the opposite without using an additional function β€” flatten, which we’ll talk about in a sec.

This means that flatMap is more general, more β€œpowerful”, than a simple map. That actually makes sense, because flatMap literally means mapping-and-then-flattening.

As with map, you have the *Indexed and *To variants, and their combination. However, there is no flatMapNotNull β€” that doesn’t make sense, because the transformation always needs to return an instance of List.

flatten

flatten is the function we need to be able to define flatMap in terms of map. It takes e.g. a List<List<T> and β€œflattens” it to a List<T>.


//sampleStart
public fun <T> Iterable<Iterable<T>>.flatten(): List<T>
//sampleEnd

Example


//sampleStart
fun main() {
    listOf(
        listOf(1, 2),
        listOf(3, 4, 5),
        listOf(6)
    ).flatten().also(::println) // listOf(1, 2, 3, 4, 5, 6)
}
//sampleEnd

Using flatten, we can implement flatMap in terms of map:


//sampleStart
inline fun <T, R> Iterable<T>.flatMap(
    transform: (T) -> Iterable<R>
): List<R> = map(transform).flatten()
//sampleEnd

More information can be found in the docs.

Set-related transformations

union, intersect, subtract


//sampleStart
public infix fun <T> Iterable<T>.union(
    other: Iterable<T>
): Set<T>
public infix fun <T> Iterable<T>.intersect(
    other: Iterable<T>
): Set<T>
public infix fun <T> Iterable<T>.subtract(
    other: Iterable<T>
): Set<T>
//sampleEnd

Both functions do exactly what they say. It’s worth pointing out that both are defined asΒ infixΒ functions.

Order-related transformations

sorted


//sampleStart
public fun <T : Comparable<T>> Iterable<T>.sorted(): List<T>
//sampleEnd

Defined only on collections of Comparable elements, sorted returns a new List, which is sorted in ascending order according to the implementation of Comparable. The sorting does not happen in place and is stable.


//sampleStart
fun main() {
    val myList = listOf(3, 5, 2)
    val sortedList = myList.sorted() // listOf(2, 3, 5)
    println(myList) // listOf(3, 5, 2)
}
//sampleEnd

There are a few sorting variants.

One is sortedBy, which allows one to specify a transformation to the elements. They will then be sorted according to the result of the transformation, which means that the result of the transformation needs to be Comparable.


//sampleStart
public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(
    crossinline selector: (T) -> R?
): List<T>
//sampleEnd

Example


//sampleStart
interface Person {
    val age: Int
}

fun sortByAge(people: List<Person>) = people.sortedBy { it.age }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the maestro 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)
}

You will notice that the transformation can return null. If it does, the element is considered smaller than all the others.

The other variant is sortedWith, which allows sorting elements which do not implement Comparable by explicitly passing a Comparator.


//sampleStart
public fun <T> Iterable<T>.sortedWith(
    comparator: Comparator<in T>
): List<T>
//sampleEnd

The compareBy function can be used to create an implementation of Comparator which compares by the values of one or multiple transformations.

Example


//sampleStart
interface Person {
    val age: Int
    val yearsDriving: Int
}

fun sortByAgeAndThenYearsDriving(people: List<Person>) = people.sortedWith(
    compareBy({ it.age }, { it.yearsDriving })
)
//sampleEnd
fun main() {
    val poem = """
        When you're sailing in the sea of code,
        Kotlin's syntax is the compass, the road.
        With waves and currents, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

All of the above have *Descending variants, which perform the sort in descending order.

reverse


//sampleStart
public fun <T> Iterable<T>.reversed(): List<T>
//sampleEnd

Returns a list with the same elements, but in reverse order.

Miscellaneous

associate


//sampleStart
public inline fun <T, K, V> Iterable<T>.associate(
    transform: (T) -> Pair<K, V>
): Map<K, V>
//sampleEnd

Transforms a collection of elements to a collections of Pairs, and creates a map out of them.

Example


//sampleStart
interface Name
interface SocialSecurityNumber
interface Person {
    val name: Name
    val ssn: SocialSecurityNumber
}

fun nameBySSNIndex(people: List<Person>): Map<SocialSecurityNumber, Name> = 
    people.associate {
        it.ssn to it.name
    }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's tower,
        With sealed classes, it builds the power.
        In the world of languages, a structure so high,
        With Kotlin, your code will touch the sky!
    """.trimIndent()
    println(poem)
}

This could be implemented in terms of map:


//sampleStart
public inline fun <T, K, V> Iterable<T>.associate(
    transform: (T) -> Pair<K, V>
): Map<K, V> = map(transform).toMap()
//sampleEnd

When duplicate keys are encountered, the last one is used.

As always, there are useful variants β€” associateBy allows one to specify a transformation which generates only the key of the resulting Map, while associateWith generates only the value. This is best understood by implementing both in terms of associate:


//sampleStart
public inline fun <T, K> Iterable<T>.associateBy(
    transform: (T) -> K
): Map<K, T> = associate { transform(it) to it }

public inline fun <T, V> Iterable<T>.associateWith(
    transform: (T) -> V
): Map<T, V> = associate { it to transform(it) }
//sampleEnd

There is also a two-parameter variant of associateBy, which allows one to specify a transformation for the value as well:


//sampleStart
public inline fun <T, K, V> Iterable<T>.associateBy(
    keyTransform: (T) -> K,
    valueTransform: (T) -> V,
): Map<K, V> = associate { keyTransform(it) to valueTransform(it) }
//sampleEnd

As always, *To variants are also included for all of the above.

zip, unzip

zip β€œzips” together two collections, to create a single List of Pairs. The returned list is as long as the shortest collection.

unzip does the opposite.


//sampleStart
public infix fun <T, R> Iterable<T>.zip(
    other: Iterable<R>
): List<Pair<T, R>>
public fun <T, R> Iterable<Pair<T, R>>.unzip(
): Pair<List<T>, List<R>>
//sampleEnd

Example

There is a really useful variant of zip that allows you to transform the resulting pair β€” in effect, it’s like applying map to the elements of two lists at once.


//sampleStart
public fun <T, R, V> Iterable<T>.zip(
    other: Iterable<R>, 
    transform: (T, R) -> V
): List<V> = (this zip other).map { (t, r) -> transform(t, r) }
//sampleEnd

Example


//sampleStart
fun main() {
    val oneList = listOf(1, 2, 3)
    val twoList = listOf("a", "b", "c")

    oneList.zip(twoList) { number, letter ->
        "$letter$number"
    }.also(::println) // listOf("a1", "b2", "c3")

}
//sampleEnd

There’s also zipWithNext:


//sampleStart
fun main() {
    val letters = ('a'..'f').toList()
    val pairs = letters.zipWithNext()
    val mergedPairs = letters.zipWithNext { l1, l2 ->
        "$l1$l2"
    }

    println(letters) // listOf('a', 'b', 'c', 'd', 'e', 'f')
    println(pairs) // listOf('a' to 'b', 'b' to 'c', 'c' to 'd', 'd' to 'e', 'e' to 'f')
    println(mergedPairs) // listOf("ab", "bc", "cd", "de", "ef")
}
//sampleEnd

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