How to transform one Scala collection to another with the ‘map’ function (method)

This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 10.14, “How to Transform One Scala Collection to Another With the map function (method)”

Problem

Like the previous recipe, you want to transform one Scala sequential collection (Seq, List, Vector, ArrayBuffer, etc.) into another by applying an algorithm to every element in the original collection.

Solution

Rather than using the for/yield combination shown in the previous recipe, call the map method on your collection, passing it a function, an anonymous function, or method to transform each element. This is shown in the following examples, where each String in a List is converted to begin with a capital letter:

scala> val helpers = Vector("adam", "kim", "melissa")
helpers: scala.collection.immutable.Vector[java.lang.String] = Vector(adam, kim, melissa)

// the long form
scala> val caps = helpers.map(e => e.capitalize)
caps: scala.collection.immutable.Vector[String] = Vector(Adam, Kim, Melissa)

// the short form
scala> val caps = helpers.map(_.capitalize)
caps: scala.collection.immutable.Vector[String] = Vector(Adam, Kim, Melissa)

The next example shows that an array of String can be converted to an array of Int:

scala> val names = Array("Fred", "Joe", "Jonathan")
names: Array[java.lang.String] = Array(Fred, Joe, Jonathan)

scala> val lengths = names.map(_.length)
lengths: Array[Int] = Array(4, 3, 8)

The map method comes in handy if you want to convert a collection to a list of XML elements:

scala> val nieces = List("Aleka", "Christina", "Molly")
nieces: List[String] = List(Aleka, Christina, Molly)

scala> val elems = nieces.map(niece => <li>{niece}</li>)
elems: List[scala.xml.Elem] = List(<li>Aleka</li>, <li>Christina</li>, <li>Molly</li>)

Using a similar technique, you can convert the collection directly to an XML literal:

scala> val ul = <ul>{nieces.map(i => <li>{i}</li>)}</ul>
ul: scala.xml.Elem = <ul><li>Aleka</li><li>Christina</li><li>Molly</li></ul>

Using anonymous functions with the map method

A function that’s passed into the map method can be as complicated as necessary. An example in the Discussion shows how to use a multiline anonymous function with map. When your algorithm gets longer, rather than using an anonymous function, define the function (or method) first, and then pass it into map:

// imagine this is a long method
scala> def plusOne(c: Char): Char = (c.toByte+1).toChar
plusOne: (c: Char)Char

scala> "HAL".map(plusOne)
res0: String = IBM

When writing a method to work with map, define the method to take a single parameter that’s the same type as the collection. In this case, plusOne is defined to take a char, because a String is a collection of Char elements, so map will operate on one Char at a time. The return type of the method can be whatever you need for your algorithm. For instance, the previous names.map(_.length) example showed that a function applied to a String can return an Int.

Unlike the for/yield approach shown in the previous recipe, the map method also works well when writing a chain of method calls. For instance, you can split a String into an array of strings, then trim the blank spaces from those strings:

scala> val s = " eggs, milk, butter, Coco Puffs "
s: String = " eggs, milk,  butter, Coco Puffs "

scala> val items = s.split(",").map(_.trim)
items: Array[String] = Array(eggs, milk, butter, Coco Puffs)

This works because split creates an Array[String], and map applies the trim method to each element in that array before returning the final array.

Discussion

For simple cases, using map is the same as using a basic for/yield loop:

scala> val people = List("adam", "kim", "melissa")
people: List[java.lang.String] = List(adam, kim, melissa)

// map
scala> val caps1 = people.map(_.capitalize)
caps1: List[String] = List(Adam, Kim, Melissa)

// for/yield
scala> val caps2 = for (f <- people) yield f.capitalize
caps2: List[String] = List(Adam, Kim, Melissa)

But once you add a guard, a for/yield loop is no longer directly equivalent to just a map method call. If you attempt to use an if statement in the algorithm you pass to a map method, you’ll get a very different result:

scala> val fruits = List("apple", "banana", "lime", "orange", "raspberry")
fruits: List[java.lang.String] = List(apple, banana, lime, orange, raspberry)

scala> val newFruits = fruits.map( f =>
     |     if (f.length < 6) f.toUpperCase
     | )
newFruits: List[Any] = List(APPLE, (), LIME, (), ())

You could filter the result after calling map to clean up the result:

scala> newFruits.filter(_ != ())
res0: List[Any] = List(APPLE, LIME)

But in this situation, it helps to think of an if statement as being a filter, so the correct solution is to first filter the collection, and then call map:

scala> val fruits = List("apple", "banana", "lime", "orange", "raspberry")
fruits: List[String] = List(apple, banana, lime, orange, raspberry)

scala> fruits.filter(_.length < 6).map(_.toUpperCase)
res1: List[String] = List(APPLE, LIME)