2016-05-01 4 views
17

Ci può essere alcun motivo per preferire filter+map:Scala: può esserci qualche ragione per preferire `filter + map` su` collect`?

list.filter (i => aCondition(i)).map(i => fun(i)) 

sopra collect? :

list.collect(case i if aCondition(i) => fun(i)) 

Quello con collect (singolo aspetto) sembra più veloce e pulito per me. Quindi andrei sempre per collect.

+0

Non sono sicuro che le prestazioni sarebbero molto diverse. Entrambi attraversano l'intera sequenza una sola volta e applicano la funzione di predicato solo agli elementi che soddisfano la condizione, quindi entrambi stanno facendo la stessa quantità di lavoro base. Dipende davvero da quanto è intelligente il compilatore, quanto è grande la sequenza e quanto costano la condizione e la funzione da eseguire. –

+0

Anche se suppongo che il secondo possa allocare una sequenza in meno, dal momento che non ha bisogno di problemi con il ritorno dalla chiamata 'filter'. –

+0

@IanMcLaird Penso che questo sia il dibattito principale. È vero che il secondo assegna una sequenza in meno (e quindi più efficiente)? – Daniel

risposta

3

Credo che questo è piuttosto opinione basa, ma date le seguenti definizioni:

scala> val l = List(1,2,3,4) 
l: List[Int] = List(1, 2, 3, 4) 

scala> def f(x: Int) = 2*x 
f: (x: Int)Int 

scala> def p(x: Int) = x%2 == 0 
p: (x: Int)Boolean 

Quale dei due fa a trovare più piacevole da leggere:

l.filter(p).map(f) 

o

l.collect{ case i if p(i) => f(i) } 

(Si noti che ho dovuto correggere la sintassi sopra, in quanto è necessario il supporto e case per aggiungere t lui condizione if).

Personalmente trovo lo filter + map molto più bello da leggere e capire. È tutta una questione di sintassi esatta che usi, ma dato p e f, non devi scrivere funzioni anonime quando usi filter o map, mentre ne hai bisogno quando usi collect.

si ha anche la possibilità di utilizzare flatMap:

l.flatMap(i => if(p(i)) Some(f(i)) else None) 

Quale è probabile che sia il più efficiente tra le 3 soluzioni, ma io lo trovo meno bello rispetto map e filter.

Nel complesso, è molto difficile dire quale sarebbe più veloce, in quanto dipende da quante ottimizzazioni vengono eseguite da scalac e poi dalla JVM. Tutti e 3 dovrebbero essere abbastanza vicini, e sicuramente non è un fattore nel decidere quale utilizzare.

+0

Grazie per aver segnalato il 'caso mancante '. Aggiustato. – Daniel

+0

Quindi, perché questo argomento non è corretto: "collect" è come un singolo ciclo, "filter + map" è come due anelli di conseguenza. Quindi (efficienza-saggio) si dovrebbe preferire "raccogliere". Mi manca qualcosa. (e buona cosa hai citato la terza alternativa.) – Daniel

+1

Non è giusto paragonare a Rex "Mr Collections" Kerr, ma nessuno si aspetterebbe che flatMap sia più efficiente. Il mio personale problema sta usando ell come nome var perché sembra uno. Ora, questo è basato sull'opinione pubblica! –

24

La maggior parte delle collezioni di Scala applica con impazienza le operazioni e (a meno che non si utilizzi una libreria di macro che fa ciò per te) non fonderà le operazioni. Quindi filter seguito da map di solito creeremo due raccolte (e anche se si utilizza Iterator o somesuch, il modulo intermedio verrà creato transitoriamente, anche se solo un elemento alla volta), mentre lo collect non lo farà.

D'altra parte, collect utilizza una funzione parziale per implementare il test congiunto e le funzioni parziali sono più lente dei predicati (A => Boolean) per verificare se qualcosa è presente nella raccolta.

Inoltre, ci possono essere casi in cui è semplicemente più chiaro leggere l'uno rispetto all'altro e non si preoccupano delle prestazioni o delle differenze di utilizzo della memoria di un fattore 2 o giù di lì. In tal caso, utilizzare quello che è più chiaro.Generalmente se avete già le funzioni di nome, è più chiaro da leggere

xs.filter(p).map(f) 
xs.collect{ case x if p(x) => f(x) } 

ma se si forniscono le chiusure in linea, collect generalmente appare più pulito

xs.filter(x < foo(x, x)).map(x => bar(x, x)) 
xs.collect{ case x if foo(x, x) => bar(x, x) } 

anche se non è necessariamente più breve, perché solo fare riferimento alla variabile una volta.

Ora, quanto è grande la differenza di prestazioni? Che varia, ma se consideriamo aa raccolta in questo modo:

val v = Vector.tabulate(10000)(i => ((i%100).toString, (i%7).toString)) 

e si vuole scegliere la seconda voce in base a filtrare il primo (in modo che le operazioni di filtro e mappa sono entrambi molto facile), allora si ottiene la seguente tabella.

Nota: è possibile ottenere visualizzazioni pigre in raccolte e raccogliere le operazioni lì. Non si ottiene sempre il tipo originale, ma è sempre possibile utilizzare to per ottenere il giusto tipo di raccolta. Quindi, xs.view.filter(p).map(f).toVector, a causa della vista, non creerebbe un intermedio. Anche questo è testato di seguito. È stato anche suggerito che si può xs.flatMap(x => if (p(x)) Some(f(x)) else None) e che questo è efficiente. Non è così. È anche testato di seguito. E si può evitare la funzione parziale creando esplicitamente un costruttore: val vb = Vector.newBuilder[String]; xs.foreach(x => if (p(x)) vb += f(x)); vb.result, ei risultati per quello sono anche elencati di seguito.

Nella tabella seguente, sono state testate tre condizioni: non filtrare nulla, filtrare la metà, filtrare tutto. I tempi sono stati normalizzati per filtrare/mappare (100% = stesso tempo di filtro/mappa, inferiore è meglio). I limiti di errore si aggirano intorno al + - 3%.

prestazioni di diverse alternative di filtro/mappa

====================== Vector ======================== 
filter/map collect view filt/map flatMap builder 
    100%  44%   64%  440%  30% filter out none 
    100%  60%   76%  605%  42% filter out half 
    100%  112%   103%  1300%  74% filter out all 

Così, filter/map e collect sono generalmente abbastanza vicino (con collect vincere quando si tiene un sacco), flatMap è di gran lunga inferiore in tutte le situazioni, e la creazione di un costruttore vince sempre. (Questo è vero in particolare per Vector. Altre collezioni possono avere caratteristiche leggermente diverse, ma le tendenze per la maggior parte saranno simili perché le differenze nelle operazioni sono simili.) Visualizzazioni in questo test tendono ad essere una vittoria, ma non funzionano sempre senza problemi (e non sono realmente migliori di collect tranne per il caso vuoto).

Quindi, linea di fondo: preferire filter quindi map se aiuta la chiarezza quando la velocità non ha importanza, o preferisci la velocità quando stai filtrando quasi tutto ma vuoi comunque mantenere le cose funzionanti (quindi non voglio usare un costruttore); e in caso contrario utilizzare collect.

+0

Per "filter none" si intende "filter out none", non "filter catches/retains none". Il primo esempio di codice opina "è più chiaro leggere [filtro e mappa]". È bello vedere una risposta così dettagliata da nord del rappresentante 100K. –

+0

Sarebbe interessante vedere come aggiungere '.view' prima che il' filter' cambi le cose. –