2010-01-19 12 views
47

In Scala gibt es eine Stream-Klasse, die einem Iterator sehr ähnlich ist. Das Thema Difference between Iterator and Stream in Scala? bietet einige Einblicke in die Ähnlichkeiten und Unterschiede zwischen den beiden.Anwendungsfälle für Streams in Scala

Sehen, wie man einen Strom benutzt, ist ziemlich einfach, aber ich habe nicht sehr viele übliche Gebrauchfälle, in denen ich einen Strom anstelle anderer Artefakte verwenden würde.

Die Ideen, die ich jetzt haben:

  • Wenn Sie brauchen, um den Einsatz einer unendlichen Reihe zu machen. Aber das scheint mir kein gewöhnlicher Anwendungsfall zu sein, daher entspricht es nicht meinen Kriterien. (Bitte korrigieren Sie mich, wenn es üblich ist und ich habe nur einen blinden Fleck)
  • Wenn Sie eine Reihe von Daten haben, bei denen jedes Element berechnet werden muss, aber Sie möchten möglicherweise mehrere Male wiederverwenden. Das ist schwach, weil ich es einfach in eine Liste laden könnte, die konzeptionell für einen großen Teil der Entwicklerpopulation einfacher zu folgen ist.
  • Vielleicht gibt es eine große Menge von Daten oder eine rechenintensive Serie und es gibt eine hohe Wahrscheinlichkeit, dass die Elemente, die Sie benötigen, nicht alle Elemente besuchen müssen. Aber in diesem Fall wäre ein Iterator eine gute Übereinstimmung, es sei denn, Sie müssen mehrere Suchen durchführen, in diesem Fall könnten Sie auch eine Liste verwenden, auch wenn sie etwas weniger effizient wäre.
  • Es gibt eine komplexe Reihe von Daten, die wiederverwendet werden müssen. Auch hier könnte eine Liste verwendet werden. In diesem Fall wären beide Fälle gleichermaßen schwierig zu verwenden und ein Stream wäre besser geeignet, da nicht alle Elemente geladen werden müssen. Aber wieder nicht so üblich ... oder?

Also habe ich irgendwelche großen Verwendungen verpasst? Oder ist es überwiegend eine Entwicklerpräferenz?

Dank

Antwort

40

Der Hauptunterschied zwischen einem Stream und einem Iterator ist, dass letztere ist wandelbar und „One-Shot“, so zu sprechen, während erstere nicht der Fall ist. Iterator hat einen besseren Speicherbedarf als Stream, aber die Tatsache, dass es veränderbar ist, kann unbequem sein.

Unternehmen Sie diesen klassischen Primzahl Generator, zum Beispiel:

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

Es kann leicht mit einem Iterator auch geschrieben werden, aber ein Iterator wird halten nicht die bisher berechneten Primzahlen.

Ein wichtiger Aspekt eines Stream ist also, dass Sie es an andere Funktionen übergeben können, ohne dass es zuerst dupliziert wird, oder dass Sie es immer wieder generieren müssen.

Wie für teure Berechnungen/unendliche Listen, können diese Dinge auch mit Iterator getan werden. Unbegrenzte Listen sind tatsächlich ziemlich nützlich - Sie wissen es einfach nicht, weil Sie es nicht hatten, also haben Sie Algorithmen gesehen, die komplexer sind als unbedingt nötig, nur um erzwungene endliche Größen zu bewältigen.

+2

Ein weiterer Unterschied, den ich hinzufügen möchte, ist, dass 'Stream' in seinem Kopfelement niemals faul ist. Der Kopf eines "Stream" wird in evaluierter Form gespeichert. Wenn eine Sequenz benötigt wird, in der kein Element (einschließlich des Kopfes) berechnet wird, bis angefordert, dann ist "Iterator" die einzige Wahl. – Lii

+0

Neben der Faulheit im Kopfelement wertet es auch jedes Element aus, das Sie löschen möchten. zB: '" a "# ::" b "# ::" c "# ::" d "# :: Stream.empy [Zeichenfolge] .drop (3)' wertet "a", "b", " c "und" d ". "d", weil es Kopf wird. – r90t

+0

Interessantes kurzes Beispiel für den Primzahlengenerator.Interessanterweise, wenn ich das in einer einfachen Scala-Konsole erzeuge und dann nach 4000 Primzahlen frage (in der Praxis nicht so viel, habe ich eine alternative Definition, die 100K in weniger als 2 Sekunden erzeugt), stürzt Scala mit einem "GC-Overhead-Limit überschritten" -Fehler ab . –

17

Zusätzlich zu Daniels Antwort, bedenken Sie, dass Stream für Kurzschlussauswertungen nützlich ist.Zum Beispiel nehme ich eine große Menge von Funktionen, die String und zurück Option[String] nehmen, und ich möchte, dass sie die Ausführung halten, bis einer von ihnen arbeitet:

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

Nun, ich will sicher nicht die auszuführen gesamte Liste, und es gibt keine handliche Methode auf List, die sagt, "behandeln diese als Funktionen und führen sie aus, bis einer von ihnen etwas anderes als None zurückgibt". Was ist zu tun? Vielleicht ist dies:

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

Dies nimmt eine Liste und behandelt sie als Stream (die eigentlich nichts bewerten), definiert dann ein neues Stream, die ein Ergebnis der Anwendung der Funktionen ist (aber das bedeutet nicht bewerten alles noch nicht), sucht dann nach dem ersten, der definiert ist - und hier, magisch, schaut es zurück und erkennt, dass es die Karte anwenden und die richtigen Daten aus der ursprünglichen Liste holen muss - und entpackt es dann aus Option[Option[String]] zu Option[String] unter Verwendung getOrElse.

Hier ist ein Beispiel:

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 

Aber funktioniert es? Wenn wir eine println in unsere Funktionen setzen, so können wir sagen, wenn sie genannt werden, wir

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 

bekommen (Dies ist mit Scala 2.8; 2.7-Implementierung wird manchmal durch ein Über-, leider Und beachten Sie, dass Sie. tun eine lange Liste von None akkumulieren als Ausfälle entstehen, aber vermutlich ist dies preiswert im Vergleich zu Ihrer wahren Berechnung hier.)

+0

Ich habe eigentlich ein solches Beispiel, aber das kann man genauso gut mit 'Iterator' machen, also entschied ich, dass es nebensächlich war. –

+2

Zugegeben. Ich hätte klarstellen müssen, dass dies nicht Stream-spezifisch ist, oder ich habe ein Beispiel ausgewählt, das mehrere Aufrufe an denselben Stream verwendet hat. –

2

Stream ist Iterator als immutable.List zu mutable.List ist. Die Bevorzugung von Unveränderlichkeit verhindert eine Klasse von Fehlern, gelegentlich auf Kosten der Leistung.

scalac selbst ist nicht immun gegen diese Probleme: http://article.gmane.org/gmane.comp.lang.scala.internals/2831

Als Daniel darlegt, kann Faulheit über Strikt begünstigende Algorithmen vereinfachen und machen es leichter, sie zu komponieren.

+1

Natürlich ist für diejenigen, die zu Faulheit neu sind, der Hauptvorbehalt, dass sie die Vorhersagbarkeit des Codes verringert, zu Heisenbugs führen kann und schwerwiegende Leistungsprobleme für einige Klassen von Algorithmen haben kann. –

7

Ich könnte mir vorstellen, dass, wenn Sie ein Gerät in Echtzeit abfragen, ein Stream bequemer ist.

Denken Sie an einen GPS-Tracker, der die tatsächliche Position zurückgibt, wenn Sie es fragen. Sie können den Ort, an dem Sie in 5 Minuten sein werden, nicht vorberechnen. Sie können es nur für ein paar Minuten verwenden, um einen Pfad in OpenStreetMap zu aktualisieren, oder Sie verwenden es für eine Expedition über sechs Monate in einer Wüste oder im Regenwald.

Oder ein digitales Thermometer oder andere Arten von Sensoren, die wiederholt neue Daten zurückgeben, solange die Hardware aktiv ist - ein Protokolldateifilter könnte ein anderes Beispiel sein.

+1

Upvoted für einen guten Anwendungsfall von Stream. – nilskp

Verwandte Themen