This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 10.11, “How to Use zipWithIndex
or zip
to Create for-Loop Counters”
Problem
You want to loop over a Scala sequential collection, and you’d like to have access to a counter in the for
loop, without having to manually create a counter.
Solution
Use the zipWithIndex
or zip
methods to create a counter automatically. Assuming you have a sequential collection of days:
val days = Array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
you can print the elements in the collection with a counter using the zipWithIndex
and foreach
methods:
days.zipWithIndex.foreach { case(day, count) => println(s"$count is $day") }
As you’ll see in the Discussion, this works because zipWithIndex
returns a series of Tuple2
elements in an Array
, like this:
Array((Sunday,0), (Monday,1), ...
and the case
statement in the foreach
loop matches a Tuple2
.
You can also use zipWithIndex
with a for
loop:
for ((day, count) <- days.zipWithIndex) { println(s"$count is $day") }
Both loops result in the following output:
0 is Sunday 1 is Monday 2 is Tuesday 3 is Wednesday 4 is Thursday 5 is Friday 6 is Saturday
When using zipWithIndex
, the counter always starts at 0
. You can also use the zip
method with a Stream
to create a counter. This gives you a way to control the starting value:
scala> for ((day,count) <- days.zip(Stream from 1)) { | println(s"day $count is $day") | }
Scala 3 Update: The Scala 3 Stream
class is deprecated, and has been replaced by the Scala 3 LazyList
class.
Discussion
When zipWithIndex
is used on a sequence, it returns a sequence of Tuple2
elements, as shown in this example:
scala> val list = List("a", "b", "c") list: List[String] = List(a, b, c) scala> val zwi = list.zipWithIndex zwi: List[(String, Int)] = List((a,0), (b,1), (c,2))
Because zipWithIndex
creates a new sequence from the existing sequence, you may want to call view
before invoking zipWithIndex
, like this:
scala> val zwi2 = list.view.zipWithIndex zwi2: scala.collection.SeqView[(String, Int),Seq[_]] = SeqViewZ(...)
As shown, this creates a lazy view on the original list, so the tuple elements won’t be created until they’re needed. Because of this behavior, calling view
before calling zipWithIndex
is recommended at the first two links in the See Also section.
However, my own experience with thousands and millions of elements — low millions, like one to ten million — concurs with the performance shown in the third link in the See Also section, where not using a view performs better. If performance is a concern, try your loop both ways, and also try manually incrementing a counter.
As mentioned, the zip
and zipWithIndex
methods both return a sequence of Tuple2
elements. Therefore, your foreach
method can also look like this:
days.zipWithIndex.foreach { d => println(s"${d._2} is ${d._1}") }
However, I think the approaches shown in the Solution are more readable.
As shown in the previous recipe, you can also use a range with a for
loop to create a counter:
val fruits = Array("apple", "banana", "orange") for (i <- 0 until fruits.size) println(s"element $i is ${fruits(i)}")
See Recipe 10.24, “Creating a Lazy View on a Collection” for more information on using views.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
- A blog post on using
zipWithIndex
in several use cases - A discussion of using
zipWithIndex
in a for loop - A discussion of performance related to using a view with
zipWithIndex
SeqView
trait