2014-09-10 21 views
12

In collezioni Scala, se si vuole iterare su una collezione (senza restituire risultati, vale a dire fare un effetto collaterale su ogni elemento della collezione), può essere fatto sia conScala: "map" vs "foreach" - c'è qualche motivo per usare "foreach" nella pratica?

final def foreach(f: (A) ⇒ Unit): Unit 

o

final def map[B](f: (A) ⇒ B): SomeCollectionClass[B] 

Con l'eccezione di possibile mappatura pigrizia (*), dal punto di vista dell'utente finale, vedo a zero differenze in queste invocazioni:

myCollection.foreach { element => 
    doStuffWithElement(element); 
} 

myCollection.map { element => 
    doStuffWithElement(element); 
} 

dato che posso semplicemente ignorare quali output della mappa. Non riesco a pensare di qualsiasi motivo specifico per cui dovrebbero esistere due metodi diversi & essere utilizzato, quando map sembra includere tutte le funzionalità di foreach, e, di fatto, sarei praticamente impressionato se un compilatore intelligente & VM non sarà ottimizzare la creazione dell'oggetto di raccolta dato che non è assegnato a nulla, né letto, né utilizzato ovunque.

Quindi, la domanda è - ho ragione - e non vi sono motivi per chiamare lo foreach in un punto qualsiasi del codice?

Note:

(*) Il concetto di mappatura pigro, as throughly illustrated in this question, potrebbe cambiare le cose un po 'e giustificare l'uso di foreach, ma per quanto posso vedere, si ha la necessità particolare di inciampare su un LazyMap, normale

(**) Se uno non sta usando una collezione, ma scrittura uno, allora si potrebbe inciampare rapidamente sul fatto che la sintassi sintassi for comprensione è in realtà uno zucchero sintassi che genera chiamata "foreach", vale a dire queste due righe generare il codice del tutto equivalente:

for (element <- myCollection) { doStuffWithElement(element); } 
myCollection.foreach { element => doStuffWithElement(element); } 

Quindi, se uno si preoccupa di altre persone che utilizzano tale classe di raccolta con for sintassi, si potrebbe ancora voglia di implementare foreach metodo.

+11

È bello usare 'foreach' invece di' map' per distinguere tra le funzioni di effetto collaterale e non di effetto collaterale. Non mi interessa se il compilatore ottimizza l'uno per l'altro. La differenza rende più evidente lo scopo del codice. –

+1

Per fare un passo avanti rispetto a LimbSoup, preferisco usare 'for ..' (sans yield) invece di' .foreach' direttamente come ulteriore (per me) mostra la divisione tra iterazione * per * effetti collaterali e una trasformazione per la sequenza. Inoltre, può essere pericoloso presumere che la sequenza che viene iterata non sia-pigra. – user2864740

+0

Al contrario, * I * sarebbe fortemente impressionato se il corrente compilatore Scala e/o la JVM sono in grado di ottimizzare la 'mappa' con il codice' foreach' se il risultato non viene utilizzato. Mi sembra una ** estremamente ** ottimizzazione non banale. – ziggystar

risposta

10

mi viene in mente un paio di motivazioni:

  1. Quando il foreach è l'ultima riga di un metodo che è di tipo Unit il compilatore non darà un avvertimento, ma sarà con map (ed è necessario -Ywarn-value-discard su). A volte si ottiene warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses utilizzando map ma non con foreach.
  2. leggibilità generale - un lettore può sapere che il vostro mutando qualche stato senza restituire qualcosa a colpo d'occhio, ma maggiori risorse cognitive sarebbe necessaria per comprendere la stessa operazione se map stato utilizzato
  3. seguito a 1. Si può anche avere tipo il controllo quando si passa funzioni denominate in giro, poi in map e foreach
6
scala> (1 to 5).iterator map println 
res0: Iterator[Unit] = non-empty iterator 

scala> (1 to 5).iterator foreach println 
1 
2 
3 
4 
5 
+0

Questo è esattamente ciò che ho menzionato in (*) - è una mappatura lazy, che è l'impostazione predefinita su tutti gli iteratori. – GreyCat

+0

Quindi la tua affermazione in questa domanda è che 'foreach' e' map' sono gli stessi tranne quando non lo sono? –

+0

La mia affermazione è che "sono gli stessi eccetto per casi chiaramente delineati (*) e (**)", entrambi i quali, a mio parere, sono abbastanza rari nella pratica media degli utenti finali. – GreyCat

3

sarei impressionato se la macchina costruttore potrebbe essere ottimizzato via.

scala> :pa 
// Entering paste mode (ctrl-D to finish) 

implicit val cbf = new collection.generic.CanBuildFrom[List[Int],Int,List[Int]] { 
def apply() = new collection.mutable.Builder[Int, List[Int]] { 
val b = new collection.mutable.ListBuffer[Int] 
override def +=(i: Int) = { println(s"Adding $i") ; b +=(i) ; this } 
override def clear() =() ; override def result() = b.result() } 
def apply(from: List[Int]) = apply() } 

// Exiting paste mode, now interpreting. 

cbf: scala.collection.generic.CanBuildFrom[List[Int],Int,List[Int]] = [email protected] 

scala> List(1,2,3) map (_ + 1) 
Adding 2 
Adding 3 
Adding 4 
res1: List[Int] = List(2, 3, 4) 

scala> List(1,2,3) foreach (_ + 1) 
+0

Per quanto posso vedere, il materiale implicito di 'CanBuildFrom' genererebbe qui una gerarchia di classi a livello di JVM, incluso quello che includerebbe il metodo' + = 'che in realtà fa qualcosa oltre a costruire una variabile che verrebbe alla fine lanciata lontano (e quindi tutto quel codice potrebbe essere saltato tranquillamente). Ovviamente, un metodo che * fa * qualcosa, specialmente quando quel qualcosa è un chiaro richiamo "invokevirtual', non sarà ottimizzato. – GreyCat