Scala: How to use zipWithIndex or zip to create loop counters

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 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.

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