Efficient iteration with index in Scala

2019-01-29 22:31发布

Since Scala does not have old Java style for loops with index,

// does not work
val xs = Array("first", "second", "third")
for (i=0; i<xs.length; i++) {
  println("String #" + i + " is " + xs(i))
}

How can we iterate efficiently, and without using var's?

You could do this

val xs = Array("first", "second", "third")
val indexed = xs zipWithIndex
for (x <- indexed) println("String #" + x._2 + " is " + x._1)

but the list is traversed twice - not very efficient.

12条回答
祖国的老花朵
2楼-- · 2019-01-29 22:58

Looping in scala is pretty simple. Create any array of your choice for ex.

val myArray = new Array[String](3)
myArray(0)="0";
myArray(1)="1";
myArray(2)="2";

Types of loops,

for(data <- myArray)println(data)

for (i <- 0 until myArray.size)
println(i + ": " + myArray(i))
查看更多
叼着烟拽天下
3楼-- · 2019-01-29 23:01

There's nothing in the stdlib that will do it for you without creating tuple garbage, but it's not too hard to write your own. Unfortunately I've never bothered to figure out how to do the proper CanBuildFrom implicit raindance to make such things generic in the type of collection they're applied to, but if it's possible, I'm sure someone will enlighten us. :)

def foreachWithIndex[A](as: Traversable[A])(f: (Int,A) => Unit) {
  var i = 0
  for (a <- as) {
    f(i, a)
    i += 1
  }
}

def mapWithIndex[A,B](in: List[A])(f: (Int,A) => B): List[B] = {
  def mapWithIndex0(in: List[A], gotSoFar: List[B], i: Int): List[B] = {
    in match {
      case Nil         => gotSoFar.reverse
      case one :: more => mapWithIndex0(more, f(i, one) :: gotSoFar, i+1)
    }
  }
  mapWithIndex0(in, Nil, 0)
}

// Tests....

@Test
def testForeachWithIndex() {
  var out = List[Int]()
  ScalaUtils.foreachWithIndex(List(1,2,3,4)) { (i, num) =>
    out :+= i * num
  }
  assertEquals(List(0,2,6,12),out)
}

@Test
def testMapWithIndex() {
  val out = ScalaUtils.mapWithIndex(List(4,3,2,1)) { (i, num) =>
    i * num
  }

  assertEquals(List(0,3,4,3),out)
}
查看更多
我命由我不由天
4楼-- · 2019-01-29 23:02

Indeed, calling zipWithIndex on a collection will traverse it and also create a new collection for the pairs. To avoid this, you can just call zipWithIndex on the iterator for the collection. This will just return a new iterator that keeps track of the index while iterating, so without creating an extra collection or additional traversing.

This is how scala.collection.Iterator.zipWithIndex is currently implemented in 2.10.3:

  def zipWithIndex: Iterator[(A, Int)] = new AbstractIterator[(A, Int)] {
    var idx = 0
    def hasNext = self.hasNext
    def next = {
      val ret = (self.next, idx)
      idx += 1
      ret
    }
  }

This should even be a bit more efficient than creating a view on the collection.

查看更多
我只想做你的唯一
5楼-- · 2019-01-29 23:02

The proposed solutions suffer from the fact that they either explicitly iterate over a collection or stuff the collection into a function. It is more natural to stick with the usual idioms of Scala and put the index inside the usual map- or foreach-methods. This can be done using memoizing. The resulting code might look like

myIterable map (doIndexed(someFunction))

Here is a way to achieve this purpose. Consider the following utility:

object TraversableUtil {
    class IndexMemoizingFunction[A, B](f: (Int, A) => B) extends Function1[A, B] {
        private var index = 0
        override def apply(a: A): B = {
            val ret = f(index, a)
            index += 1
            ret
        }
    }

    def doIndexed[A, B](f: (Int, A) => B): A => B = {
        new IndexMemoizingFunction(f)
    }
}

This is already all you need. You can apply this for instance as follows:

import TraversableUtil._
List('a','b','c').map(doIndexed((i, char) => char + i))

which results in the list

List(97, 99, 101)

This way, you can use the usual Traversable-functions at the expense of wrapping your effective function. Enjoy!

查看更多
闹够了就滚
6楼-- · 2019-01-29 23:04

Actually, scala has old Java-style loops with index:

scala> val xs = Array("first","second","third")
xs: Array[java.lang.String] = Array(first, second, third)

scala> for (i <- 0 until xs.length)
     | println("String # " + i + " is "+ xs(i))

String # 0 is first
String # 1 is second
String # 2 is third

Where 0 until xs.length or 0.until(xs.length) is a RichInt method which returns Range suitable for looping.

Also, you can try loop with to:

scala> for (i <- 0 to xs.length-1)
     | println("String # " + i + " is "+ xs(i))
String # 0 is first
String # 1 is second
String # 2 is third
查看更多
Root(大扎)
7楼-- · 2019-01-29 23:05

Some more ways to iterate:

scala>  xs.foreach (println) 
first
second
third

foreach, and similar, map, which would return something (the results of the function, which is, for println, Unit, so a List of Units)

scala> val lens = for (x <- xs) yield (x.length) 
lens: Array[Int] = Array(5, 6, 5)

work with the elements, not the index

scala> ("" /: xs) (_ + _) 
res21: java.lang.String = firstsecondthird

folding

for(int i=0, j=0; i+j<100; i+=j*2, j+=i+2) {...}

can be done with recursion:

def ijIter (i: Int = 0, j: Int = 0, carry: Int = 0) : Int =
  if (i + j >= 100) carry else 
    ijIter (i+2*j, j+i+2, carry / 3 + 2 * i - 4 * j + 10) 

The carry-part is just some example, to do something with i and j. It needn't be an Int.

for simpler stuff, closer to usual for-loops:

scala> (1 until 4)
res43: scala.collection.immutable.Range with scala.collection.immutable.Range.ByOne = Range(1, 2, 3)

scala> (0 to 8 by 2)   
res44: scala.collection.immutable.Range = Range(0, 2, 4, 6, 8)

scala> (26 to 13 by -3)
res45: scala.collection.immutable.Range = Range(26, 23, 20, 17, 14)

or without order:

List (1, 3, 2, 5, 9, 7).foreach (print) 
查看更多
登录 后发表回答