zipWith (mapeamento sobre múltiplos Seq) em Scala
-
18-09-2019 - |
Pergunta
Suponha que eu tenho
val foo : Seq[Double] = ...
val bar : Seq[Double] = ...
e gostaria de produzir uma seq onde o baz (i) = foo (i) + bar (i). Uma maneira que eu posso pensar de fazer isso é
val baz : Seq[Double] = (foo.toList zip bar.toList) map ((f: Double, b : Double) => f+b)
No entanto, este se sente tanto feia e ineficiente - Eu tenho que converter ambos os PDCs para listas (que explode com listas preguiçoso), criar esta lista temporária de tuplas, apenas para mapear sobre ele e deixá-lo ser GCed. Talvez córregos resolver o problema preguiçoso, mas em qualquer caso, este se sente como desnecessariamente feio. Em Lisp, a função de mapa seria mapear ao longo de várias sequências. Eu ia escrever
(mapcar (lambda (f b) (+ f b)) foo bar)
E há listas de temporário se tornasse criado em qualquer lugar. Existe um mapa-over-múltiplas listas de funcionar em Scala, ou seja zip combinada com desestruturação realmente o caminho 'certo' para fazer isso?
Solução
A função desejada é chamado zipWith
, mas não é uma parte da biblioteca padrão. Será em 2.8. (UPDATE: Aparentemente não, ver comentários)
foo zipWith((f: Double, b : Double) => f+b) bar
Outras dicas
Em Scala 2.8:
val baz = (foo, bar).zipped map (_ + _)
E trabalha há mais de dois operandos da mesma forma. Ou seja, Você poderia, então, acompanhar esta questão com:
(foo, bar, baz).zipped map (_ * _ * _)
Bem, isso, a falta de zip, é uma deficiência em 2,7 Seq do Scala. Scala 2.8 tem um design coleção bem-pensamento, para substituir o modo ad-hoc as coleções presente em 2,7 veio a ser (note que eles não foram todos criados de uma só vez, com um design unificado).
Agora, quando você quer evitar a criação de coleção temporária, você deve usar "projeção" em Scala 2.7, ou "vista" em Scala 2.8. Isto lhe dará um tipo de coleção para a qual certas instruções, particularmente mapear, flatMap e filtro, não são rigorosos. Em Scala 2,7, a projecção de uma lista é um Stream. Em Scala 2.8, há um SequenceView de uma seqüência, mas há uma zipWith ali mesmo na seqüência, você nem sequer precisa.
Dito isto, como mencionado, JVM é otimizado para lidar com alocações de objetos temporários, e, quando executado no modo servidor, a otimização de tempo de execução pode fazer maravilhas. Então, não otimizar prematuramente. Teste o código nas condições que serão executados - e se você não tem planejado para executá-lo no modo de servidor, em seguida, repensar que, se o código está prevista para ser de longa duração, e optmize quando / onde / se necessário <. / p>
Editar
O que é realmente vai estar disponível no Scala 2.8 é o seguinte:
(foo,bar).zipped.map(_+_)
Uma lista preguiçoso não é uma cópia de uma lista - é mais como um único objeto. No caso de uma implementação zip preguiçoso, cada vez que é solicitado para o próximo item, ele agarra um item de cada uma das duas listas de entrada e cria uma tupla a partir deles, e você, em seguida, quebrar o tuple distante com o padrão de correspondência em seu lambda.
Assim, nunca há uma necessidade de criar uma cópia completa da lista de entrada inteira (s) antes de começar a operar sobre eles. Ela resume-se a um padrão de alocação muito semelhante a qualquer aplicativo em execução no JVM -. Lotes de alocações de muito curta duração, mas pequenas, que a JVM é otimizado para lidar com
Update: para ser claro, você precisa estar usando Streams (listas preguiçosos) não Lists. fluxos de Scala tem um fecho de correr que funciona da maneira preguiçosa, e por isso você não deve ser converter as coisas em listas.
O ideal é o seu algoritmo deve ser capaz de trabalhar em dois infinito córregos sem explodir (assumindo que não faz qualquer folding
, é claro, mas apenas lê e gera fluxos).
UPDATE: Ele tem sido apontado (nos comentários) que esta "resposta" na verdade não abordar a questão que se coloca. Esta resposta vai mapear sobre cada combinação de foo
e bar
, produzindo N x M elementos, em vez de o min (M, N) , conforme solicitado . Então, este é errado , mas deixou para a posteridade, já que é uma boa informação.
A melhor maneira de fazer isso é com flatMap
combinado com map
. Código fala mais alto do que palavras:
foo flatMap { f => bar map { b => f + b } }
Isto irá produzir uma única Seq[Double]
, exatamente como seria de esperar. Esse padrão é tão comum que Scala, na verdade, inclui um pouco de magia sintática que implementa-lo:
for {
f <- foo
b <- bar
} yield f + b
Ou, alternativamente:
for (f <- foo; b <- bar) yield f + b
A sintaxe for { ... }
é realmente a maneira mais idiomática de fazer isso. Você pode continuar a adicionar cláusulas gerador (por exemplo b <- bar
) conforme necessário. Assim, se ele de repente se torna três Seq
s que você deve mapear longo, você pode facilmente escalar sua sintaxe, juntamente com os seus requisitos (para cunhar uma frase).
Quando enfrentou uma tarefa semelhante, eu adicionei o seguinte cafetão para 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
}
}
}
Tendo isso, pode fazer algo como:
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)
}
Observe que você deve considerar cuidadosamente tipo de coleção passada como Iterable
aninhada, desde tail
e head
serão recorrentemente chamada nele. Assim, idealmente você deve passar Iterable[List]
ou outra coleção com tail
rápido e head
.
Além disso, este código espera coleções aninhadas do mesmo tamanho. Esse foi o meu caso de uso, mas eu suspeito que isso pode ser melhorada, se necessário.