ZipWith (отображение через несколько Seq) в Scala

Предположим, что

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

и я хочу создать seq, где 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 функция отображения отображает несколько последовательностей. Я бы написал

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

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

Ответ 1

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

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

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

Ответ 2

В Scala 2.8:

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

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

(foo, bar, baz).zipped map (_ * _ * _)

Ответ 3

Ну, это, отсутствие zip, является недостатком в Scala 2.7 Seq. Scala 2.8 имеет продуманный дизайн коллекции, чтобы заменить специальный способ, с которым пришли коллекции, представленные в версии 2.7 (обратите внимание, что они не были созданы сразу, с единым дизайном).

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

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

ИЗМЕНИТЬ

Что на самом деле будет доступно на Scala 2.8:

(foo,bar).zipped.map(_+_)

Ответ 4

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

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

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

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

Ответ 5

Когда вы столкнулись с аналогичной задачей, я добавил следующий pimp к 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.

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

Ответ 6

ОБНОВЛЕНИЕ: Было указано (в комментариях), что этот "ответ" на самом деле не отвечает на заданный вопрос. Этот ответ будет отображать каждую комбинацию 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, которые вы должны перечислить, вы можете легко масштабировать свой синтаксис вместе с вашими требованиями (чтобы развить фразу).