scala stream scala-collections

Casos de uso para Streams en Scala



scala-collections (4)

En Scala hay una clase Stream que se parece mucho a un iterador. El tema ¿ Diferencia entre iterador y corriente en Scala? ofrece algunas ideas sobre las similitudes y diferencias entre los dos.

Ver cómo usar una transmisión es bastante simple, pero no tengo muchos casos de uso comunes en los que usaría una transmisión en lugar de otros artefactos.

Las ideas que tengo ahora mismo:

  • Si necesitas hacer uso de una serie infinita. Pero esto no me parece un caso de uso común, por lo que no se ajusta a mis criterios. (Por favor corríjame si es común y tengo un punto ciego)
  • Si tiene una serie de datos en los que se debe calcular cada elemento, pero es posible que desee reutilizarlos varias veces. Esto es débil porque podría cargarlo en una lista que sea conceptualmente más fácil de seguir para un gran subconjunto de la población de desarrolladores.
  • Quizás exista un gran conjunto de datos o una serie computacionalmente costosa y existe una alta probabilidad de que los elementos que necesita no requieran visitar todos los elementos. Pero en este caso, un iterador sería una buena coincidencia a menos que necesite hacer varias búsquedas, en ese caso también podría usar una lista, incluso si fuera un poco menos eficiente.
  • Hay una serie compleja de datos que necesitan ser reutilizados. Una vez más una lista podría ser utilizada aquí. Aunque en este caso ambos casos serían igualmente difíciles de usar y un Stream sería un mejor ajuste ya que no es necesario cargar todos los elementos. Pero de nuevo no es tan común ... ¿o no?

Entonces, ¿me he perdido algún gran uso? ¿O es una preferencia del desarrollador en su mayor parte?

Gracias


Además de la respuesta de Daniel, tenga en cuenta que Stream es útil para evaluaciones de cortocircuito. Por ejemplo, supongamos que tengo un gran conjunto de funciones que toman la String y devuelven la Option[String] , y quiero seguir ejecutándolas hasta que una de ellas funcione:

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

Bueno, ciertamente no quiero ejecutar la lista completa , y no hay ningún método práctico en la List que diga, "trátelos como funciones y ejecútelos hasta que uno de ellos devuelva algo que None sea None ". ¿Qué hacer? Quizás esto:

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

Esto toma una lista y la trata como una Stream (que en realidad no evalúa nada), luego define una nueva Stream que es el resultado de la aplicación de las funciones (pero que todavía no evalúa nada), y luego busca la primera uno que está definido, y aquí, mágicamente, mira hacia atrás y se da cuenta de que tiene que aplicar el mapa y obtener los datos correctos de la lista original, y luego lo desenvuelve de la Option[Option[String]] a la Option[String] usando getOrElse .

Aquí hay un ejemplo:

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

¿Pero funciona? Si ponemos una println en nuestras funciones para que podamos saber si se llaman, obtenemos

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

(Esto es con Scala 2.8; la implementación de 2.7 a veces se sobrepasa en uno, desafortunadamente. Y tenga en cuenta que acumula una larga lista de None medida que sus fallas se acumulan, pero presumiblemente esto no es caro en comparación con su cálculo real).


Podría imaginar que si sondea algún dispositivo en tiempo real, un Stream es más conveniente.

Piensa en un rastreador de GPS, que devuelve la posición real si lo pides. No se puede calcular el lugar donde estará en 5 minutos. Puede usarlo durante unos minutos solo para actualizar una ruta en OpenStreetMap o puede usarla para una expedición de más de seis meses en un desierto o en la selva tropical.

O un termómetro digital u otro tipo de sensores que devuelven repetidamente datos nuevos, siempre que el hardware esté activo y encendido, un filtro de archivo de registro podría ser otro ejemplo.


Stream es a Iterator como immutable.List es a mutable.List . mutable.List . Favorecer la inmutabilidad previene una clase de errores, ocasionalmente a costa del rendimiento.

Scalac en sí no es inmune a estos problemas: http://article.gmane.org/gmane.comp.lang.scala.internals/2831

Como señala Daniel, favorecer la pereza sobre el rigor puede simplificar los algoritmos y hacer que sea más fácil componerlos.


La principal diferencia entre un Stream y un Iterator es que el último es mutable y "one-shot", por así decirlo, mientras que el primero no lo es. Iterator tiene una mejor huella de memoria que Stream , pero el hecho de que sea mutable puede ser inconveniente.

Tome este clásico generador de números primos, por ejemplo:

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

También se puede escribir fácilmente con un Iterator , pero un Iterator no mantendrá los números primos calculados hasta ahora.

Por lo tanto, un aspecto importante de un Stream es que puede pasarlo a otras funciones sin tener que duplicarlo primero o tener que generarlo una y otra vez.

En cuanto a costosos cálculos / listas infinitas, estas cosas también se pueden hacer con Iterator . Las listas infinitas son en realidad bastante útiles: simplemente no lo sabe porque no las tenía, por lo que ha visto algoritmos que son más complejos de lo estrictamente necesario solo para lidiar con los tamaños finitos impuestos.