2009-07-21 15 views
30

Supponiamo che io sonozipWith (mappatura su più Seq) in Scala

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

e desidero produrre un ss dove il baz (i) = foo (i) + bar (i). Un modo posso pensare di fare questo è

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

Tuttavia, questo si sente sia brutto e inefficiente - devo convertire sia seguenti del regolamento provvisorio per le liste (che esplode con le liste pigri), creare questa lista provvisoria di tuple, solo per mappare su di esso e lasciare che sia GCed. Forse i flussi risolvono il problema pigro, ma in ogni caso, sembra inutilmente brutto. In Lisp, la funzione mappa verrebbe mappata su più sequenze. Vorrei scrivere

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

E nessuna lista temporanea sarebbe stata creata ovunque. Esiste una funzione map-over-multiple-lists in Scala, oppure la compressione combinata con destrutturazione è davvero il modo "giusto" per farlo?

risposta

15

La funzione desiderata è denominata zipWith, ma non fa parte della libreria standard. Sarà in 2.8 (AGGIORNAMENTO: Apparentemente no, vedi commenti).

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

Vedere this Trac ticket.

+2

Siamo spiacenti, nessun file zipWith su Scala 2.8. –

+3

Giusto per essere chiari (e sono sicuro che Daniel sarebbe d'accordo), Scala non ha nulla da scusarsi per questo - quello che ottieni con Scala è ancora meglio. Vedi la risposta di Martin in basso, e quella di Daniel. Sarebbe bello se qualcuno potesse dare a Martin la risposta approvata a questa domanda ... – AmigoNico

3

Un elenco pigro non è una copia di un elenco: è più simile a un singolo oggetto. Nel caso di un'implementazione lazy zip, ogni volta che viene richiesto l'elemento successivo, prende un oggetto da ognuna delle due liste di input e crea una tupla da loro, quindi si rompe la tupla con l'abbinamento di motivi in il tuo lambda.

Quindi non è mai necessario creare una copia completa dell'intero elenco di input prima di iniziare a utilizzarli. Si riduce ad un modello di allocazione molto simile a qualsiasi applicazione in esecuzione sulla JVM: molte allocazioni di breve durata ma di piccole dimensioni, che la JVM è ottimizzata per affrontare.

Aggiornamento: per essere chiari, è necessario utilizzare Streams (elenchi pigri) non elenchi. Gli stream di Scala hanno uno zip che funziona in modo pigro e quindi non dovresti convertire le cose in elenchi.

Idealmente il vostro algoritmo dovrebbe essere in grado di lavorare su due infinite flussi senza far saltare in aria (ammesso che non fa alcun folding, naturalmente, ma solo legge e genera flussi).

+0

So che cos'è una lista pigra, ma non ho molta familiarità con Scala. foo.toList non è pigro, giusto? In ogni caso, provenendo da uno sfondo CL, è molto strano che non ci sia una funzione Map più grande, quindi la mia ragione per fare questa domanda è solo per capire quale sia il modo corretto di Scala per farlo. Le prestazioni sono in realtà abbastanza importanti; questo è nel mio ciclo interno e, mentre posso provare a ottimizzarlo in un secondo momento, vorrei prima immetterlo in codice in modo ragionevole. – bsdfish

+0

Ti dico che eri corretto quando hai detto "forse i flussi risolvono il problema" - usa la versione di streaming di zip. Se pensate che piccole allocazioni stanno facendo pressione sul GC, scrivete un equivalente imperativo nel linguaggio basato su JVM di vostra scelta, e calcolateli per vedere se è vero (sono stato spesso stupito dai brillanti dei VM che si occupano di lotti di piccole allocazioni di breve durata). –

9

Bene, questo, la mancanza di zip, è una carenza di 2.7 Seq della Scala. Scala 2.8 ha un design della collezione ben pensato, in sostituzione del modo ad hoc che le collezioni presenti in 2.7 sono diventate (notare che non sono state tutte create contemporaneamente, con un design unificato).

Ora, quando si desidera evitare la creazione di una raccolta temporanea, è necessario utilizzare "proiezione" su Scala 2.7 o "visualizzazione" su Scala 2.8. Questo ti darà un tipo di raccolta per il quale alcune istruzioni, in particolare la mappa, flatMap e filtro, non sono rigide. Su Scala 2.7, la proiezione di una lista è un flusso. Su Scala 2.8, c'è una SequenceView di una Sequence, ma c'è uno zipWith proprio lì nella Sequence, non ne avresti nemmeno bisogno.

Detto questo, come detto, JVM è ottimizzato per gestire allocazioni temporanee di oggetti e, quando è in esecuzione in modalità server, l'ottimizzazione in fase di esecuzione può fare miracoli. Quindi, non ottimizzarlo prematuramente.Verificare il codice nelle condizioni in cui verrà eseguito e, se non si è pianificato di eseguirlo in modalità server, ripensare che se il codice è previsto essere di lunga durata e optmize quando/dove/se necessario.

EDIT

Ciò che è in realtà sta per essere disponibile su Scala 2.8 è questo:

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

UPDATE: E 'stato fatto notare (nei commenti) che questa "risposta" doesn in realtà indirizza la domanda che viene posta. Questa risposta sarà mappare su ogni combinazione di foo e bar, producendo N x M elementi, al posto del min (M, N) come richiesto. Quindi, questo è errato, ma lasciato per i posteri poiché è una buona informazione.


Il modo migliore per farlo è con flatMap combinato con map. Codice parla più forte delle parole:

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

Questo produrrà un unico Seq[Double], esattamente come ci si aspetterebbe. Questo modello è così comune che Scala in realtà comprende una certa magia sintattica che implementa:

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

O, in alternativa:

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

Il for { ... } sintassi è davvero il modo più idiomatico per fare questo. Puoi continuare ad aggiungere clausole di generatore (ad esempio b <- bar) se necessario. Pertanto, se diventa improvvisamente treSeq s su cui è necessario eseguire la mappatura, è possibile ridimensionare facilmente la sintassi insieme alle proprie esigenze (per coniare una frase).

+4

Per ora non voterò per questa domanda, ma è completamente sbagliato. Ciò si tradurrà in elementi NxN, e ciò che la domanda ha chiesto risultati in soli N elementi. Stai aggiungendo ogni combinazione di elementi da foo e bar, ma ciò che viene chiesto è foo (i) + bar (i). –

+1

Buon punto. Era un po 'presto la mattina, quindi apparentemente il mio cervello non funzionava correttamente. Cancellerò questa risposta, poiché in realtà non fornisce ciò che l'autore stava chiedendo. –

+1

In realtà, aggiornerò la risposta. È una buona informazione, solo non applicabile a questa domanda. –

74

In Scala 2.8:

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

E funziona per più di due operandi nello stesso modo. Cioè si potrebbe poi seguire questo con:

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

Non sembra funzionare con più di tre operandi, tuttavia. È corretto? – Debilski

+14

Corretto, 'zipped' è definito solo su' Tuple2' e 'Tuple3'. L'astrazione sull'arit è una delle ultime frontiere di Scala (e della maggior parte delle altre lingue scritte in modo statico). Le liste H offrono una possibilità ... – retronym

+7

@retronym c'è anche l'approccio '<*>'/'<$>' che utilizziamo con ZipList in Haskell, in cui non è necessario astrarre l'arit in base all'omogeneità delle funzioni al curry. Quindi, se volessi zipWith con un parametro di 5 parametri 'f', potrei fare più o meno' f <$> xs <*> ys <*> zs <*> ps <*> qs'. Purtroppo le funzioni al curry sono molto più dolorose da trattare in Scala :(forse alcune idee potrebbero essere trasferite, dal momento che questo approccio sembra sostanzialmente più elegante di quello "HList" –

1

Di fronte un compito simile, ho aggiunto il seguente magnaccia per 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 
    } 
    } 
} 

Avendo questo, si può fare qualcosa di simile:

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

Si noti che è necessario considerare attentamente il tipo di raccolta passato come nidificato Iterable, poiché tail e head verranno ripetutamente richiamati i t. Quindi, idealmente dovresti passare la collezione Iterable[List] o other con il veloce tail e head.

Inoltre, questo codice prevede collezioni nidificate della stessa dimensione. Quello era il mio caso d'uso, ma sospetto che questo possa essere migliorato, se necessario.

Problemi correlati