zipWith (отображение на несколько Seq) в Scala - PullRequest
34 голосов
/ 21 июля 2009

Предположим, у меня есть

val foo : Seq[Double] = ...
val bar : Seq[Double] = ...

и я хочу произвести последовательность, в которой baz (i) = foo (i) + bar (i). Один из способов сделать это -

val baz : Seq[Double] = (foo.toList zip bar.toList) map ((f: Double, b : Double) => f+b)

Однако это кажется уродливым и неэффективным - мне нужно преобразовать оба seqs в списки (которые взрываются с ленивыми списками), создать этот временный список кортежей, только чтобы отобразить его и позволить ему быть GCed. Возможно, потоки решают ленивую проблему, но в любом случае это выглядит излишне безобразным. В lisp функция map будет отображаться в нескольких последовательностях. Я бы написал

(mapcar (lambda (f b) (+ f b)) foo bar)

И никакие временные списки не будут созданы нигде. Есть ли в Scala функция отображения по нескольким спискам, или zip в сочетании с деструктуризацией действительно является «правильным» способом сделать это?

Ответы [ 6 ]

81 голосов
/ 18 июня 2010

В Scala 2.8:

val baz = (foo, bar).zipped map (_ + _)

И это работает более чем для двух операндов одинаково. То есть затем вы можете выполнить следующие действия:

(foo, bar, baz).zipped map (_ * _ * _)
16 голосов
/ 21 июля 2009

Функция, которую вы хотите, называется zipWith, но она не является частью стандартной библиотеки. Это будет в 2.8 (ОБНОВЛЕНИЕ: Видимо, нет, см. Комментарии).

foo zipWith((f: Double, b : Double) => f+b) bar

См. этот билет Trac .

9 голосов
/ 21 июля 2009

Что ж, отсутствие почтового индекса является недостатком в 2.7 Seq. Scala 2.8 имеет продуманный дизайн коллекций, чтобы заменить случайный подход к коллекциям, представленным в 2.7 (обратите внимание, что не все они были созданы сразу, с единым дизайном).

Теперь, когда вы хотите избежать создания временной коллекции, вы должны использовать «проекцию» в Scala 2.7 или «просмотр» в Scala 2.8. Это даст вам тип коллекции, для которой определенные инструкции, в частности, map, flatMap и filter, не являются строгими. На Scala 2.7 проекция списка - это поток. В Scala 2.8 есть SequenceView для Sequence, но в Sequence есть zipWith, и он вам даже не понадобится.

Сказав, что, как уже упоминалось, JVM оптимизирована для обработки временных распределений объектов, и при работе в режиме сервера оптимизация во время выполнения может творить чудеса. Так что не стоит оптимизировать преждевременно. Протестируйте код в условиях, когда он будет запущен - и если вы не планировали запускать его в режиме сервера, то подумайте, что, если код будет работать долго, и выберите optmize, когда / где / при необходимости.

EDIT

То, что на самом деле будет доступно в Scala 2.8, это:

(foo,bar).zipped.map(_+_)
3 голосов
/ 21 июля 2009

Ленивый список не является копией списка - он больше похож на один объект. В случае с отложенной реализацией zip, каждый раз, когда его просят о следующем элементе, он выбирает элемент из каждого из двух входных списков и создает из них кортеж, а затем разбивает кортеж с помощью сопоставления с шаблоном в твоя лямбда.

Таким образом, перед началом работы с ними никогда не требуется создавать полную копию всего списка входных данных. Он сводится к очень похожему шаблону распределения для любого приложения, работающего на JVM - множество очень коротких, но небольших распределений, с которыми JVM оптимизирована для работы.

Обновление: , чтобы быть понятным, вам нужно использовать потоки (ленивые списки), а не списки. В потоках Scala есть zip-архив, который работает ленивым образом, поэтому вам не следует конвертировать вещи в списки.

В идеале ваш алгоритм должен быть способен работать с двумя бесконечными потоками без взрыва (при условии, что он, конечно, не folding, а просто читает и генерирует потоки).

1 голос
/ 12 августа 2015

Столкнувшись с подобной задачей, я добавил в Iterable s следующее сутенер:

implicit class IterableOfIterablePimps[T](collOfColls: Iterable[Iterable[T]]) {
  def mapZipped[V](f: Iterable[T] => V): Iterable[V] = new Iterable[V] {
    override def iterator: Iterator[V] = new Iterator[V] {
      override def next(): V = {
        val v = f(itemsLeft.map(_.head))
        itemsLeft = itemsLeft.map(_.tail)
        v
      }

      override def hasNext: Boolean = itemsLeft.exists(_.nonEmpty)

      private var itemsLeft = collOfColls
    }
  }
}

Имея это, можно сделать что-то вроде:

val collOfColls = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))
collOfColls.mapZipped { group =>
  group // List(1, 4, 7), then List(2, 5, 8), then List(3, 6, 9)
}

Обратите внимание, что вы должны внимательно рассмотреть тип коллекции, переданный как вложенный Iterable, поскольку для него будут периодически вызываться tail и head. Итак, в идеале вы должны передать Iterable[List] или другой набор с быстрыми tail и head.

Кроме того, этот код ожидает вложенные коллекции одинакового размера. Это был мой вариант использования, но я подозреваю, что это может быть улучшено, если это необходимо.

0 голосов
/ 21 июля 2009

ОБНОВЛЕНИЕ: Было отмечено (в комментариях), что этот "ответ" на самом деле не касается задаваемого вопроса. Этот ответ будет отображаться для каждой комбинации из foo и bar, производя N x M элементов вместо min (M, N) как просил. Итак, это неправильно , но оставлено для потомков, поскольку это хорошая информация.


Лучший способ сделать это с flatMap в сочетании с map. Код говорит громче, чем слова:

foo flatMap { f => bar map { b => f + b } }

Это даст один Seq[Double], именно так, как вы ожидаете. Этот шаблон настолько распространен, что Scala включает некоторую синтаксическую магию, которая его реализует:

for {
  f <- foo
  b <- bar
} yield f + b

Или, альтернативно:

for (f <- foo; b <- bar) yield f + b

Синтаксис for { ... } действительно самый идиоматический способ сделать это. При необходимости вы можете продолжить добавлять предложения генератора (например, b <- bar). Таким образом, если вам вдруг потребуется три Seq с, которые вы должны отобразить, вы можете легко масштабировать свой синтаксис вместе с вашими требованиями (чтобы добавить фразу).

...