2010-01-19 4 views
47

In Scala esiste una classe Stream che è molto simile a un iteratore. L'argomento Difference between Iterator and Stream in Scala? offre alcune informazioni sulle somiglianze e le differenze tra i due.Casi d'uso per gli stream in Scala

Vedere come utilizzare un flusso è abbastanza semplice ma non ho molti comuni casi di utilizzo in cui vorrei utilizzare uno stream anziché altri artefatti.

Le idee che ho in questo momento:

  • Se avete bisogno di fare uso di una serie infinita. Ma questo non mi sembra un caso d'uso comune, quindi non corrisponde ai miei criteri. (Si prega di correggermi se è comune e ho solo un punto cieco)
  • Se si dispone di una serie di dati in cui ogni elemento deve essere calcolato ma che si potrebbe desiderare di riutilizzare più volte. Questo è debole perché potrei semplicemente caricarlo in una lista che è concettualmente più facile da seguire per un grosso sottogruppo della popolazione di sviluppatori.
  • Forse c'è una grande serie di dati o una serie computazionalmente costosa e c'è un'alta probabilità che gli elementi necessari non richiederanno la visita di tutti gli elementi. Ma in questo caso un Iterator sarebbe una buona corrispondenza a meno che non sia necessario fare diverse ricerche, in tal caso si potrebbe usare anche un elenco anche se sarebbe leggermente meno efficiente.
  • Esiste una serie complessa di dati che è necessario riutilizzare. Di nuovo una lista potrebbe essere usata qui. Anche se in questo caso entrambi i casi sarebbero ugualmente difficili da usare e uno Stream sarebbe una misura migliore dal momento che non tutti gli elementi devono essere caricati. Ma ancora non è così comune ... o è vero?

Quindi ho perso qualche grande utilizzo? O è una preferenza per gli sviluppatori per la maggior parte?

Grazie

risposta

40

La differenza principale tra un Stream ed un Iterator è che quest'ultimo è mutevole e "one-shot", per così dire, mentre la prima non è. Iterator ha un ingombro di memoria migliore di Stream, ma il fatto che sia è modificabile può essere sconveniente.

prendere questo generatore di classico numero primo, per esempio:

def primeStream(s: Stream[Int]): Stream[Int] = 
    Stream.cons(s.head, primeStream(s.tail filter { _ % s.head != 0 })) 
val primes = primeStream(Stream.from(2)) 

Si può facilmente essere scritto con un Iterator pure, ma un Iterator non manterrà i numeri primi calcolati finora.

Quindi, un aspetto importante di un Stream è che è possibile passarlo ad altre funzioni senza averlo prima duplicato o doverlo generare ancora e ancora.

Per quanto riguarda costosi calcoli/liste infinite, queste cose possono essere fatte anche con Iterator. Gli elenchi infiniti sono in realtà abbastanza utili: non lo sai perché non ce l'hai, quindi hai visto algoritmi più complessi del necessario solo per far fronte a dimensioni finite forzate.

+2

Un'altra differenza che vorrei aggiungere è che 'Stream' non è mai pigro nel suo elemento principale. La testa di un 'Stream' è memorizzata in forma valutata. Se uno ha bisogno di una sequenza in cui nessun elemento (compresa la testa) viene calcolato fino a quando richiesto, 'Iterator' è l'unica scelta. – Lii

+0

Oltre alla non-pigrizia dell'elemento head, valuta anche ogni elemento che vuoi eliminare. es .: '" a "# ::" b "# ::" c "# ::" d "# :: Stream.empy [String] .drop (3)' valuterà "a", "b", " c "e" d ". "d" perché diventa testa. – r90t

+0

Esempio succinto interessante per il generatore di numeri primi.È interessante notare che se lo creo in una semplice console di Scala e poi chiedo 4000 numeri primi (non tanto nella pratica, ho una definizione alternativa che crea 100K in meno di 2 secondi) si blocca Scala con un errore "Limite di sovraccarico GC superato" . –

17

Oltre alla risposta di Daniel, tenere presente che Stream è utile per le valutazioni di cortocircuito.Ad esempio, supponiamo di avere un enorme insieme di funzioni che prendono String e restituiscono Option[String], e voglio tenerli esecuzione fino a quando uno di loro lavora:

val stringOps = List(
    (s:String) => if (s.length>10) Some(s.length.toString) else None , 
    (s:String) => if (s.length==0) Some("empty") else None , 
    (s:String) => if (s.indexOf(" ")>=0) Some(s.trim) else None 
); 

Beh, io certamente non voglio eseguire il Tutta la lista, e non c'è alcun metodo a portata di mano su List che dice, "trattarli come funzioni ed eseguirli finché uno di essi restituisce qualcosa di diverso da None". Cosa fare? Forse questo:

def transform(input: String, ops: List[String=>Option[String]]) = { 
    ops.toStream.map(_(input)).find(_ isDefined).getOrElse(None) 
} 

Questo richiede una lista e lo tratta come un Stream (che in realtà non valutare nulla), quindi definisce una nuova Stream che è il risultato di applicare le funzioni (ma che non valutano qualcosa ancora), quindi cerca il primo che è definito - e qui, magicamente, guarda indietro e si rende conto che deve applicare la mappa, e ottenere i dati giusti dall'elenco originale - e poi scartalo da Option[Option[String]] a Option[String] utilizzando getOrElse.

Ecco un esempio:

scala> transform("This is a really long string",stringOps) 
res0: Option[String] = Some(28) 

scala> transform("",stringOps) 
res1: Option[String] = Some(empty) 

scala> transform(" hi ",stringOps) 
res2: Option[String] = Some(hi) 

scala> transform("no-match",stringOps) 
res3: Option[String] = None 

Ma funziona? Se mettiamo un println nelle nostre funzioni in modo che possiamo dire se si chiamano, otteniamo

val stringOps = List(
    (s:String) => {println("1"); if (s.length>10) Some(s.length.toString) else None }, 
    (s:String) => {println("2"); if (s.length==0) Some("empty") else None }, 
    (s:String) => {println("3"); if (s.indexOf(" ")>=0) Some(s.trim) else None } 
); 
// (transform is the same) 

scala> transform("This is a really long string",stringOps) 
1 
res0: Option[String] = Some(28) 

scala> transform("no-match",stringOps)      
1 
2 
3 
res1: Option[String] = None 

(Questo è con Scala 2.8; implementazione 2.7 di superamento a volte per uno, purtroppo E notare che è. fare accumulare una lunga lista di None come i tuoi fallimenti maturano, ma presumibilmente questo è poco costoso rispetto al vostro vero calcolo qui.)

+0

In realtà sono un esempio del genere, ma questo può essere fatto con 'Iterator', quindi ho deciso che era fuori questione. –

+2

concesso. Avrei dovuto chiarire che questo non è specifico per Stream, o scelto un esempio che usava più chiamate allo stesso Stream. –

2

Stream è di Iterator come immutable.List è quello di mutable.List. Favorire l'immutabilità previene una classe di bug, a volte a scapito delle prestazioni.

scalac stessa non è immune a questi problemi: http://article.gmane.org/gmane.comp.lang.scala.internals/2831

Come Daniel sottolinea, favorendo la pigrizia su rigore in grado di semplificare procedure e rendere più facile per comporre loro.

+1

Naturalmente, per chi è nuovo alla pigrizia, l'avvertenza principale è che riduce la prevedibilità del codice, può portare a heisenbugs e può avere gravi problemi di prestazioni per alcune classi di algoritmi. –

7

Posso immaginare, che se si esegue il polling di alcuni dispositivi in ​​tempo reale, un flusso è più conveniente.

Pensa a un localizzatore GPS, che restituisce la posizione attuale se lo chiedi. Non è possibile precomputare la posizione in cui ci si troverà tra 5 minuti. Potresti usarlo per alcuni minuti solo per attualizzare un percorso in OpenStreetMap o potresti usarlo per una spedizione di oltre sei mesi in un deserto o nella foresta pluviale.

Oppure un termometro digitale o altri tipi di sensori che restituiscono ripetutamente nuovi dati, a condizione che l'hardware sia attivo e acceso - un filtro dei file di registro potrebbe essere un altro esempio.

+1

In aumento per un buon utilizzo di Stream. – nilskp