Table of Contents
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)
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |