Table of Contents
This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 10.13, “How to Transform One Scala Collection to Another with for/yield”
Problem
You want to create a new collection from an existing collection by transforming the elements with an algorithm.
Solution
Use the for/yield construct and your algorithm to create the new collection. For instance, starting with a basic collection:
scala> val a = Array(1, 2, 3, 4, 5) a: Array[Int] = Array(1, 2, 3, 4, 5)
You can create a copy of that collection by just “yielding” each element (with no algorithm):
scala> for (e <- a) yield e res0: Array[Int] = Array(1, 2, 3, 4, 5)
You can create a new collection where each element is twice the value of the original:
scala> for (e <- a) yield e * 2 res1: Array[Int] = Array(2, 4, 6, 8, 10)
You can determine the modulus of each element:
scala> for (e <- a) yield e % 2 res2: Array[Int] = Array(1, 0, 1, 0, 1)
This example converts a list of strings to uppercase:
scala> val fruits = Vector("apple", "banana", "lime", "orange") fruits: Vector[String] = Vector(apple, banana, lime, orange) scala> val ucFruits = for (e <- fruits) yield e.toUpperCase ucFruits: Vector[String] = Vector(APPLE, BANANA, LIME, ORANGE)
Your algorithm can return whatever collection is needed. This approach converts the original collection into a sequence of Tuple2
elements:
scala> for (i <- 0 until fruits.length) yield (i, fruits(i)) res0: scala.collection.immutable.IndexedSeq[(Int, String)] = Vector((0,apple), (1,banana), (2,lime), (3,orange))
This algorithm yields a sequence of Tuple2
elements that contains each original string along with its length:
scala> for (f <- fruits) yield (f, f.length) res1: Vector[(String, Int)] = Vector((apple,5), (banana,6), (lime,4), (orange,6))
If your algorithm takes multiple lines, include it in a block after the yield:
scala> val x = for (e <- fruits) yield { | // imagine this required multiple lines | val s = e.toUpperCase | s | } x: Vector[String] = List(APPLE, BANANA, LIME, ORANGE)
Given a Person
class and a list of friend’s names like this:
case class Person (name: String) val friends = Vector("Mark", "Regina", "Matt")
a for/yield loop can yield a collection of Person
instances:
scala> for (f <- friends) yield Person(f) res0: Vector[Person] = Vector(Person(Mark), Person(Regina), Person(Matt))
You can include if statements (guards) in a for-comprehension to filter elements:
scala> val x = for (e <- fruits if e.length < 6) yield e.toUpperCase x: List[java.lang.String] = List(APPLE, LIME)
Discussion
This combination of a for
loop and yield
statement is known as a for comprehension or sequence comprehension. It yields a new collection from an existing collection.
For readability, I’ll refer to “for comprehension” as “for-comprehension.”
If you’re new to using the for/yield construct, it can help to think that is has a bucket or temporary holding area on the side. As each element from the original collection is operated on with yield
and your algorithm, it’s added to that bucket. Then, when the for
loop is finished iterating over the entire collection, all of the elements in the bucket are returned (yielded) by the expression.
In general, the collection type that’s returned by a for-comprehension will be the same type that you begin with. If you begin with an ArrayBuffer
, you’ll end up with an ArrayBuffer
:
scala> val fruits = scala.collection.mutable.ArrayBuffer("apple", "banana") fruits: scala.collection.mutable.ArrayBuffer[java.lang.String] = ArrayBuffer(apple, banana) scala> val x = for (e <- fruits) yield e.toUpperCase x: scala.collection.mutable.ArrayBuffer[java.lang.String] = ArrayBuffer(APPLE, BANANA)
A List
returns a List
:
scala> val fruits = "apple" :: "banana" :: "orange" :: Nil fruits: List[java.lang.String] = List(apple, banana, orange) scala> val x = for (e <- fruits) yield e.toUpperCase x: List[java.lang.String] = List(APPLE, BANANA, ORANGE)
However, as shown in the Solution, this isn’t always the case.
Using guards
When you add guards to a for-comprehension and want to write it as a multiline expression, the recommended coding style is to use curly braces rather than parentheses:
for { file <- files if hasSoundFileExtension(file) if !soundFileIsLong(file) } yield file
This makes the code more readable, especially when the list of guards becomes long. See Recipe 3.3, “Using a for Loop with Embedded if Statements (Guards)”, more information on using guards.
When using guards, the resulting collection can end up being a different size than the input collection:
scala> val cars = Vector("Mercedes", "Porsche", "Tesla") cars: Vector[String] = Vector(Mercedes, Porsche, Tesla) scala> for { | c <- cars | if c.startsWith("M") | } yield c res0: Vector[String] = Vector(Mercedes)
In fact, if none of the car names had matched the startsWith
test, that code would return an empty Vector
.
When I first started working with Scala I always used a for/yield expression to do this kind of work, but one day the light bulb went on and I realized that I could achieve the same result more concisely using the
map
method. The next recipe demonstrates how to usemap
to create a new collection from an existing collection.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
- Recipe 3.1, “How to loop with for and foreach in Scala”, provides detailed examples of how for loops are translated by the Scala compiler into
foreach
and map method calls - Recipe 3.3, “How to use a Scala for Loop with Embedded if Statements (Guards)”, provides more examples of using guards