Wydajna iteracja z indeksem w Scali

83

Ponieważ Scala nie ma starych forpętli w stylu Java z indeksem,

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

Jak możemy wydajnie iterować bez używania var?

Możesz to zrobić

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

ale lista jest przeglądana dwukrotnie - niezbyt wydajna.

żwawy
źródło
To wszystko są dobre odpowiedzi. To, czego brakuje mi w pętlach „for” w Javie, to możliwość posiadania wielu inicjatorów oraz możliwość „iteracji” przy użyciu czegoś więcej niż tylko inkrementacji / dekrementacji. Jest to jeden przypadek, w którym Java może być bardziej zwięzła niż Scala.
zgryźliwy
... "iteruj" używając więcej niż tylko inkrementacji / dekrementacji ... W scali możliwe jest iterowanie z krokiem lub iteracja z warunkiem "if" w nagłówku pętli. A może szukasz czegoś innego?
om-nom-nom
1
/ * Java * / for (int i = 0, j = 0; i + j <100; i + = j * 2, j + = i + 2) {...} Jak możesz to zrobić w 1 linii w Scali?
zgryźliwy
3
@snappy: Moim zdaniem najbardziej naturalnym tłumaczeniem na Scala byłaby whilepętla. Jak sobie przypominam, kilka lat temu odbyła się debata, czy Scala powinna odziedziczyć for(;;)pętlę Javy i zdecydowano, że korzyści nie są wystarczające, aby uzasadnić dodatkową złożoność.
Kipton Barros

Odpowiedzi:

132

Znacznie gorzej niż dwukrotne przechodzenie, tworzy pośredni zestaw par. Możesz użyć view. Kiedy to zrobisz collection.view, możesz myśleć, że kolejne wywołania działają leniwie podczas iteracji. Jeśli chcesz odzyskać w pełni zrealizowaną kolekcję, dzwonisz forcena koniec. Tutaj byłoby to bezużyteczne i kosztowne. Więc zmień swój kod na

for((x,i) <- xs.view.zipWithIndex) println("String #" + i + " is " + x)
Didier Dupont
źródło
6
Fajny pomysł, tylko jedno przejście, ale tworzy też n par, nawet jeśli nie tworzy nowej właściwej kolekcji.
zgryźliwy
2
Całkiem dobrze. Cóż, może istnieć niejasna nadzieja, że ​​JVM może zoptymalizować te tworzenie, ale nie liczyłbym na to. Nie widzę rozwiązania, które nie byłoby wtedy oparte na iterowaniu po indeksach.
Didier Dupont
1
@snappy Ten powinien zostać wybrany jako odpowiedź! Dostęp do elementów za pomocą indeksu, który sugerowano w większości innych odpowiedzi, narusza funkcjonalny charakter Scali i fatalnie działa na połączonych listach (jak Listnajczęściej używana kolekcja w Scali) - i nie tylko na nich. Sprawdź tutajapply operację . W kolekcji połączonej z listą każdy dostęp do elementu za pomocą indeksu powoduje przejście przez listę.
Nikita Volkov
pokazane są tutaj całkiem inne podejścia: stackoverflow.com/questions/6821194/…
Neil
Dlaczego jest to wydajne? tworzy nowy obiekt tablicy i używa dodatkowej funkcji („widoku”), więc trudno mi zrozumieć, dlaczego jest to wydajne dla programisty ani dla maszyny, poza poczuciem inteligentnej idiomatyczności.
matanster
70

Wspomniano już, że Scala nie ma składni forpętli:

for (i <- 0 until xs.length) ...

lub po prostu

for (i <- xs.indices) ...

Jednak prosiłeś również o wydajność. Okazuje się, że Scala forskładnia jest rzeczywiście cukier syntaktyczny dla wyższego rzędu, takich jak metody map, foreachitd Jako takie, w niektórych przypadkach te pętle mogą być nieskuteczne, np jak zoptymalizować dla-listowe i pętli w Scala?

(Dobra wiadomość jest taka, że ​​zespół Scala pracuje nad poprawieniem tego. Oto problem w narzędziu do śledzenia błędów: https://issues.scala-lang.org/browse/SI-4633 )

Aby uzyskać najwyższą wydajność, można użyć whilepętli lub, jeśli nalegasz na usunięcie zastosowań var, rekurencji ogonowej:

import scala.annotation.tailrec

@tailrec def printArray(i: Int, xs: Array[String]) {
  if (i < xs.length) {
    println("String #" + i + " is " + xs(i))
    printArray(i+1, xs)
  }
}
printArray(0, Array("first", "second", "third"))

Zauważ, że opcjonalna @tailrec adnotacja jest przydatna, aby upewnić się, że metoda jest rzeczywiście rekurencyjna. Kompilator Scala tłumaczy wywołania rekurencyjne tail na kod bajtowy odpowiednika pętli while.

Kipton Barros
źródło
+1 za wspominanie o metodzie / funkcji indeksów, ponieważ uważam, że jest to preferowane, ponieważ praktycznie eliminuje cały zestaw błędów w programowaniu.
chaotic3quilibrium 27.07.11
1
Należy tutaj zauważyć, że jeśli xsjest to jakaś lista połączona (taka jak szeroko stosowana List), dostęp do jej elementów za pomocą indeksu xs(i)będzie liniowy, a zatem for (i <- xs.indices) println(i + " : " + xs(i))będzie działał znacznie gorzej niż nawet for((x, i) <- xs.zipWithIndex) println(i + " : " + x), ponieważ spowoduje znacznie więcej niż tylko dwie trawersy pod maską. Dlatego odpowiedź @didierd sugerująca użycie poglądów należy przyjąć jako najbardziej ogólną i najbardziej idiomatyczną, IMO.
Nikita Volkov
1
Jeśli potrzebna jest maksymalna wydajność (na przykład w obliczeniach numerycznych), indeksowanie tablic jest szybsze niż przeglądanie połączonej listy. Węzły połączonej listy są oddzielnie przydzielane na sterty, a przeskakiwanie między różnymi lokalizacjami pamięci nie działa dobrze z pamięcią podręczną procesora. Jeśli viewzostanie użyty a, ten nawet wysoki poziom abstrakcji będzie wywierał większy nacisk na stertę i GC. Z mojego doświadczenia wynika, że ​​często osiąga się współczynnik 10, który można uzyskać unikając alokacji sterty w kodzie numerycznym.
Kipton Barros
20

Jeszcze jeden sposób:

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

scala> for (i <- xs.indices)
     |   println(i + ": " + xs(i))
0: first
1: second
2: third
missingfaktor
źródło
5
Bardzo podoba mi się, jak wskazujesz metodę / funkcję indeksów. Zmniejsza złożoność i praktycznie eliminuje cały zestaw błędów „wyłącz o jeden”, które są najczęstszym błędem / błędem programowania w całej inżynierii oprogramowania.
chaotic3quilibrium
14

Właściwie scala ma stare pętle w stylu Java z indeksem:

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

Gdzie 0 until xs.lengthlub 0.until(xs.length)jest RichIntmetodą, która zwraca Rangeodpowiednią do zapętlenia.

Możesz także wypróbować pętlę za pomocą 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
mniam mniam mniam
źródło
5
xs(i)na listach podnosi złożoność do O (n ^ 2)
Vadzim
@Vadzim To prawda, ale tak by się stało w Javie, w którym użyłeś pętli for na indeksach z LinkedList
francoisr
1
W przypadku xs (i) na Arrays, powyższy kod to O (n), prawda? Ponieważ tablice w scali oferują dostęp losowy w niemal stałym czasie?
dhfromkorea
2
@dhfromkorea tak, powinno być szybkie dla tablic (rzeczywiście O (n))
om-nom-nom
6

Co powiesz na to?

val a = Array("One", "Two", "Three")
a.foldLeft(0) ((i, x) => {println(i + ": " + x); i + 1;} )

Wynik:

0: One
1: Two
2: Three
Matthew Saltz
źródło
4

Zapętlenie w scali jest dość proste. Utwórz dowolną tablicę, np.

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

Rodzaje pętli,

for(data <- myArray)println(data)

for (i <- 0 until myArray.size)
println(i + ": " + myArray(i))
Prakhyat
źródło
4

Rzeczywiście, wywołanie zipWithIndexkolekcji przejdzie przez nią, a także stworzy nową kolekcję dla par. Aby tego uniknąć, możesz po prostu wywołać zipWithIndexiterator kolekcji. Spowoduje to po prostu zwrócenie nowego iteratora, który śledzi indeks podczas iteracji, więc bez tworzenia dodatkowej kolekcji lub dodatkowego przechodzenia.

Oto jak scala.collection.Iterator.zipWithIndexjest obecnie zaimplementowane w 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
    }
  }

Powinno to być nawet bardziej wydajne niż tworzenie widoku kolekcji.

Hermana
źródło
3

W stdlib nie ma nic, co zrobi to za Ciebie bez tworzenia krotek śmieci, ale nie jest zbyt trudne napisanie własnego. Niestety nigdy nie zadałem sobie trudu, aby dowiedzieć się, jak zrobić właściwy CanBuildFrom ukryty taniec, aby takie rzeczy były ogólne w typie kolekcji, do której są stosowane, ale jeśli to możliwe, jestem pewien, że ktoś nas oświeci. :)

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)
}
Alex Cruise
źródło
Jest to coś, co z pewnością będzie miało sens, jeśli zostanie dodane do standardowej biblioteki.
zgryźliwy
1
Nie jestem taki pewien, ponieważ jeśli chcesz dostosować się do zwykłych interfejsów API foreach / map, i tak utkniesz z krotkami.
Alex Cruise
3

Więcej sposobów na iterację:

scala>  xs.foreach (println) 
first
second
third

foreach i podobna mapa, która zwróciłaby coś (wyniki funkcji, która jest dla println, Unit, a więc List of Units)

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

pracować z elementami, a nie indeksem

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

składanie

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

można zrobić z rekurencją:

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) 

Część przenosząca to tylko przykład zrobienia czegoś z i i j. Nie musi to być Int.

dla prostszych rzeczy, bliżej zwykłych pętli for:

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)

lub bez zamówienia:

List (1, 3, 2, 5, 9, 7).foreach (print) 
nieznany użytkownik
źródło
3

Mam następujące podejścia

object HelloV2 {

   def main(args: Array[String]) {

     //Efficient iteration with index in Scala

     //Approach #1
     var msg = "";

     for (i <- args.indices)
     {
       msg+=(args(i));
     }
     var msg1="";

     //Approach #2
     for (i <- 0 until args.length) 
     {
       msg1 += (args(i));
     }

     //Approach #3
     var msg3=""
     args.foreach{
       arg =>
        msg3 += (arg)
     }


      println("msg= " + msg);

      println("msg1= " + msg1);

      println("msg3= " + msg3);

   }
}
anish
źródło
2

Prosty i skuteczny sposób, zainspirowany wdrożeniem transformw SeqLike.scala

    var i = 0
    xs foreach { el =>
      println("String #" + i + " is " + xs(i))
      i += 1
    }
Adrian
źródło
0

Proponowane rozwiązania obarczone są tym, że albo jawnie iterują po kolekcji, albo umieszczają ją w funkcji. Bardziej naturalne jest trzymanie się zwykłych idiomów Scali i umieszczanie indeksu wewnątrz zwykłych metod map- lub foreach. Można to zrobić za pomocą zapamiętywania. Wynikowy kod może wyglądać tak

myIterable map (doIndexed(someFunction))

Oto sposób na osiągnięcie tego celu. Rozważ następujące narzędzie:

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)
    }
}

To już wszystko, czego potrzebujesz. Możesz to zastosować na przykład w następujący sposób:

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

co powoduje wyświetlenie listy

List(97, 99, 101)

W ten sposób możesz używać zwykłych funkcji Traversable kosztem opakowywania efektywnej funkcji. Cieszyć się!

Matt Brasen
źródło