paint-brush
Map Transformation: Which One Should I Use?by@darrylbayliss
105 reads

Map Transformation: Which One Should I Use?

by Darryl BaylissJuly 9th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Learn how map transformations work within the Kotlin Standard Library.
featured image - Map Transformation: Which One Should I Use?
Darryl Bayliss HackerNoon profile picture

Which Map Transformation Should I Use?

Map transformation functions find common usage in Android development. They are part of the Kotlin Standard Library, a library built by JetBrains to provide standard functionality across Kotlin codebases.


Inside the Standard Library is a package called kotlin.collections, containing the building blocks for different collections. These include Lists, Maps, and Sets.


The collections package also contains the map transformation functions. These functions take the contents of a collection and transform them into another collection containing the transformed state.


A diagram of how map transformations work



Let’s take a look at some examples.

The Map Transformation

The first transformation is the map() function.


  val numbersList = listOf(1, 2, 3)
  numbersList.map { it + 1 }.also(::println) // listOf(2, 3, 4)

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  numbersMap.map { it.value + 1 }.also(::println) // listOf(2, 3, 4)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.map { it + 1 }.also(::println) // listOf(2, 3, 4)


This function iterates over a collection and applies the transformation to each value within a lambda, before returning a new collection containing the transformed values.


map() is available across different types of collections. The reason for this is because map() is an extension function on the Iterable interface. Most collections implement Iterable, meaning they can make use of the map function.


Map collection types are different. They don’t implement Iterable, instead, they possess a separate extension method to provide a map function that iterates over each entry and returns a List of results.

Map Transformations and Null Values

Map transformations come in different forms. Another useful transformation is the mapNotNull() function.


  val numbersList = listOf(1, 2, 3)
  numbersList.mapNotNull { if (it + 1 == 3) null else it + 1  }.also(::println) // listOf(2, 4)

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)

  numbersMap.mapNotNull { if (it.value + 1 == 3) null else it.value + 1 }.also(::println) // listOf(2, 4)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.mapNotNull { if (it + 1 == 3) null else it + 1 }.also(::println) // listOf(2, 4)


This function acts both as a transformation function and a filter for null values. If the transformation inside the lambda results in null, then the value is not added to the new list.


mapNotNull() is available to collections implementing the Iterable interface.


Map types have their own extension function to provide similar functionality. Returning a list of results.

Acquiring an Index with Map Transformations

If you need to know the location of the value within the collection being transformed, you can use mapIndexed().


  val numbersList = listOf(1, 2, 3)
  numbersList.mapIndexed { index, number -> number + index + 1 }.also(::println) // listOf(2, 4, 6)

  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.asIterable().mapIndexed { index, entry -> entry.value + index + 1 }.also(::println) // listOf(2, 4, 6)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.mapIndexed { index, number -> number + index + 1 }.also(::println) // listOf(2, 4, 6)


Here, the location of the value (the index) within the collection is passed alongside the value being transformed. mapIndexed() is available to collections implementing the Iterable interface.


Map types don’t have a mapIndexed() extension function. What you can do though is use the asIterable() extension to wrap the Map inside an Iterable instance. Then you can use mapIndexed() without issue.


If you need to check for null values and also require an index, you can also use mapIndexedNotNull().


  val numbersList = listOf(1, 2, 3)
  numbersList.mapIndexedNotNull { index, number -> if (number + 1 == 3) null else number + index + 1 }.also(::println) // listOf(2, 6)
  
  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.asIterable().mapIndexedNotNull { index, entry -> if (entry.value + 1 == 3) null else entry.value + index + 1 }.also(::println) // listOf(2, 6)

  val numbersSet = setOf(1, 2, 3)
  numbersSet.mapIndexedNotNull { index, number -> if (number + 1 == 3) null else number + index + 1 }.also(::println) //listOf(2, 6)


mapIndexedNotNull() works similarly to mapNotNull(). It filters away null values within the transformation lambda whilst also passing in the index for the value from the collection.


Like other map transformations, it exists on all types implementing Iterable. Map types can use the asIterable() function to gain access to mapIndexedNotNull().

Other Transformations for Map Types

Map types work differently than other collections due to not implementing the Collection or Iterable interfaces. Because of this, they have a few of their own transformation functions not available to other types.


The first is called mapKeys().


  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.mapKeys { entry -> entry.key.capitalize() }.also(::println) // mapOf("One" to 1, "Two" to 2, "Three" to 3)


mapKeys() transforms each key within the map by passing through each Entry of the map. Once all the transformations are complete, they are applied to the Map.


The second function is called mapValues().


  val numbersMap = mapOf("one" to 1, 
                         "two" to 2, 
                         "three" to 3)
  
  numbersMap.mapValues { entry -> entry.value + 1 }.also(::println) // mapOf("one" to 2, "two" to 3, "three" to 4)


mapValues() works in a similar way. It passes through each Entry of the map and transforms each value. Once all the transformations are complete they are applied to the map.

Passing Map Transformations to a Destination

If you want to pass applied transformations to a different collection other than the source, there are a few functions to help.


  val numbersList = listOf(1, 2, 3)
  val numbersDestinationSet = mutableSetOf<Int>()
  numbersList.mapTo(numbersDestinationSet) { it + 1 }
  println(numbersDestinationSet) // setOf(2, 3, 4)

  val numbersSet = setOf(1, 2, 3)
  val numbersDestinationList = mutableListOf<Int>()
  numbersSet.mapTo(numbersDestinationList) { it + 1 }
  println(numbersDestinationList) // listOf(2, 3, 4)


The mapTo functions work similarly to map(). The difference is they write the transformations to the passed-in collection. The collection being written doesn’t have to be the same type as the source collection. Useful if you have a usecase where a different collection would be more optimal.


Map types can’t use mapTo(). This is because mapTo() expects to write to a MutableCollection, which Map types don’t inherit from. There is a MutableMap type, however, because it doesn’t inherit from MutableCollection there is no mapTo() extension available.

More Resources

If you’d like to learn more about Kotlin’s transformation methods. I highly recommend this page on collection transformations from the Kotlin language documentation. As well as covering map transformations it also covers other methods like zipping, association, and flattening. These topics are beyond the scope of this post, however, they are nevertheless useful to understand.