Ефективна ітерація з індексом у Scala


83

Оскільки Scala не має старих forциклів стилю Java з індексом,

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

Як ми можемо виконувати ітерацію ефективно і без використання varсимволів?

Ви могли б це зробити

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

але список обходить двічі - не дуже ефективно.


Це все хороші відгуки. Що мені не вистачає в циклах Java for - це можливість мати кілька ініціалізаторів та можливість "ітерації", використовуючи не просто інкременти / декременти. Це один приклад, коли Java може бути більш стислим, ніж Scala.
швидкий

... "ітерація", використовуючи більше, ніж просто інкременти / декременти ... У масштабі можна виконати ітерацію за кроком або ітерацію за умови "якщо" у заголовку циклу. Або ви шукаєте щось інше?
om-nom-nom

1
/ * Java * / for (int i = 0, j = 0; i + j <100; i + = j * 2, j + = i + 2) {...} Як це можна зробити в одному рядку в Scala?
стрімкий

3
@snappy: На мій погляд, найбільш природним перекладом на Scala буде whileцикл. Як я пам’ятаю, кілька років тому відбулася дискусія, чи Scala повинна успадкувати for(;;)цикл Java , і було вирішено, що вигоди недостатньо для виправдання додаткової складності.
Kipton Barros

Відповіді:


130

Набагато гірше, ніж проїзд двічі, він створює посередницький масив пар. Можна використовувати view. Коли ви це зробите collection.view, ви можете думати про наступні дзвінки як про ліниві дії під час ітерації. Якщо ви хочете повернути належну повністю реалізовану колекцію, телефонуйте forceв кінці. Тут це було б марно і дорого. Тож змініть свій код на

for((x,i) <- xs.view.zipWithIndex) println("String #" + i + " is " + x)

6
Приємна ідея, лише один обхід, але він також створює n пар, навіть якщо він не створює нову власну колекцію.
швидкий

2
Абсолютно вірно. Ну, може бути неясна надія, що JVM може оптимізувати ці твори, але я б не розраховував на це. Я не бачу рішення, яке б тоді не базувалося на ітерації індексів.
Didier Dupont

1
@snappy Цього слід було вибрати як відповідь! Доступ до елементів за допомогою індексу, що пропонувалось у більшості інших відповідей, порушує функціональну природу Scala і страшно працює у зв’язаних списках (наприклад List, у найбільш часто використовуваній колекції в Scala) - і не тільки в них. Перевірте applyоперацію тут . У зв’язаній подібній до списку колекції кожен доступ до елемента за індексом призводить до обходу списку.
Микита Волков

тут показані зовсім інші підходи: stackoverflow.com/questions/6821194/…
Ніл

Чому це ефективно? він створює новий об'єкт масиву і використовує додаткову функцію (`view '), тому мені важко зрозуміти, чому це ефективно ні для розробника, ні для машини, окрім того, що я відчуваю себе розумно ідіоматично.
matanster

69

Було відзначено , що Scala робить має синтаксис для forпетель:

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

або просто

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

Однак ви також просили про ефективність. Виявляється, forсинтаксис Scala насправді є синтаксичним цукром для таких методів вищого порядку, як map, foreachтощо. Як такі, в деяких випадках ці цикли можуть бути неефективними, наприклад Як оптимізувати для розуміння та циклів у Scala?

(Хороша новина полягає в тому, що команда Scala працює над покращенням цього. Ось проблема у програмі відстеження помилок: https://issues.scala-lang.org/browse/SI-4633 )

Для максимальної ефективності можна використовувати whileцикл або, якщо ви наполягаєте на видаленні використання var, рекурсію хвоста:

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

Зауважте, що необов’язкова @tailrec анотація корисна для того, щоб переконатися, що метод насправді є рекурсивним. Компілятор Scala переводить зворотно-рекурсивні виклики в еквівалент байтового коду циклів while.


+1 для згаданого методу / функції індексів, оскільки я вважаю це кращим, оскільки він практично усуває цілий набір помилок програмування, що випадають окремо.
chaotic3quilibrium

1
Тут слід зазначити, що якщо xsє будь-який зв’язаний список (наприклад, широко використовуваний List), доступ до його елементів за допомогою індексу типу xs(i)буде лінійним і, отже, for (i <- xs.indices) println(i + " : " + xs(i))буде працювати гірше ніж навіть for((x, i) <- xs.zipWithIndex) println(i + " : " + x), оскільки це призведе до набагато більше, ніж просто дві обходи під капотом. Тому відповідь @didierd, що пропонує використовувати точки зору, слід сприймати як найбільш загальну та найбільш ідіоматичну, IMO.
Микита Волков

1
Якщо потрібна максимальна ефективність (наприклад, в числових обчисленнях), швидше індексувати масиви, ніж обходити пов'язаний список. Вузли зв'язаного списку виділяються окремо в купі, і перехід через різні місця пам'яті погано грає з кешем процесора. Якщо viewвикористовується a , цей навіть високий рівень абстракції зробить більший тиск на купу і ГХ. З мого досвіду, часто коефіцієнт 10 у продуктивності можна отримати, уникаючи розподілу купи в числовому коді.
Кіптон Баррос,

20

Ще один спосіб:

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

5
Мені дуже подобається, що ви вказуєте на метод / функцію індексів. Це зменшує складність і практично усуває цілий набір помилок "вимкнено одним", що є найпоширенішою помилкою / помилкою програмування у всіх програмних інженеріях.
chaotic3quilibrium

14

Насправді у Scala є старі цикли в стилі Java з індексом:

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

Де 0 until xs.lengthабо 0.until(xs.length)є RichIntметод, який повертається, Rangeпридатний для циклу.

Крім того, ви можете спробувати виконати цикл за допомогою 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

5
xs(i)у списках підвищує складність до O (n ^ 2)
Вадзім

@Vadzim Це правда, але це було б і у Java, коли ви використовували цикл for на індексах з LinkedList
francoisr

1
У випадку xs (i) на масивах, наведений вище код - O (n), так? Оскільки масиви в Scala пропонують майже постійний час довільного доступу?
dhfromkorea

2
@dhfromkorea так, для Arrays має бути швидким (справді O (n))
om-nom-nom

6

Як щодо цього?

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

Вихід:

0: One
1: Two
2: Three

4

Цикл у шкалі досить простий. Створіть будь-який масив на ваш вибір для екс.

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

Типи петель,

for(data <- myArray)println(data)

for (i <- 0 until myArray.size)
println(i + ": " + myArray(i))

4

Справді, виклик zipWithIndexколекції пересече її, а також створить нову колекцію для пар. Щоб уникнути цього, ви можете просто зателефонувати zipWithIndexдо ітератора для збору. Це просто поверне новий ітератор, який відстежує індекс під час ітерації, тому без створення додаткової колекції або додаткового обходу.

Ось як scala.collection.Iterator.zipWithIndexзараз реалізовано в 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
    }
  }

Це має бути навіть трохи ефективніше, ніж створення уявлення про колекцію.


3

У stdlib немає нічого, що зробить це за вас, не створюючи кортежне сміття, але не надто складно написати власне. На жаль, я ніколи не намагався зрозуміти, як зробити належний CanBuildF від неявного дощів, щоб зробити такі речі загальними для типу колекції, до якої вони застосовуються, але якщо це можливо, я впевнений, що хтось нас просвітлить. :)

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

Це те, що, безумовно, має сенс додати до стандартної бібліотеки.
стрімкий

1
Я не настільки впевнений, оскільки якщо ви хочете відповідати звичним API-інтерфейсам foreach / map, ви все одно застрягли в кортежах.
Alex Cruise,

3

Ще кілька способів ітерації:

scala>  xs.foreach (println) 
first
second
third

foreach та подібна до них карта, яка б щось повернула (результати функції, яка для println є Unit, тобто Список одиниць)

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

робота з елементами, а не з індексом

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

складаний

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

можна зробити з рекурсією:

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) 

Частина перенесення - це лише якийсь приклад, щоб зробити щось із i та j. Це не повинно бути Int.

для більш простих речей, ближчих до звичайних 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)

або без замовлення:

List (1, 3, 2, 5, 9, 7).foreach (print) 

3

У мене є такі підходи

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

   }
}

2

Простий та ефективний спосіб, натхненний реалізацією transformв SeqLike.scala

    var i = 0
    xs foreach { el =>
      println("String #" + i + " is " + xs(i))
      i += 1
    }

0

Запропоновані рішення страждають від того, що вони або явно перебирають колекцію, або набивають колекцію у функцію. Більш природно дотримуватися звичних ідіом Scala та вносити індекс всередину звичних методів map- або foreach. Це можна зробити за допомогою мемоізування. Отриманий код може виглядати так

myIterable map (doIndexed(someFunction))

Ось спосіб досягнення цієї мети. Розглянемо таку утиліту:

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

Це вже все, що вам потрібно. Ви можете застосувати це, наприклад, наступним чином:

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

що призводить до переліку

List(97, 99, 101)

Таким чином, ви можете використовувати звичайні Traversable-функції за рахунок обтікання вашої ефективної функції. Насолоджуйтесь!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.