2010-01-19 15 views
47

En Scala hay una clase Stream que se parece mucho a un iterador. El tema Difference between Iterator and Stream in Scala? ofrece algunas ideas sobre las similitudes y diferencias entre los dos.Casos de uso para flujos en Scala

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

Las ideas que tengo en este momento:

  • Si tienen que hacer uso de una serie infinita. Pero esto no me parece un caso de uso común, así que no se ajusta a mis criterios. (Por favor corrígeme si es común y solo tengo un punto ciego)
  • Si tiene una serie de datos donde cada elemento necesita ser computado pero que puede reutilizarlo varias veces. Esto es débil porque podría cargarlo en una lista que es conceptualmente más fácil de seguir para un gran subconjunto de la población de desarrolladores.
  • Quizás haya un gran conjunto de datos o una serie computacionalmente costosa y existe una gran 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 podría usar una lista incluso si fuera un poco menos eficiente.
  • Hay una serie compleja de datos que deben reutilizarse. De nuevo, una lista podría usarse aquí. Aunque en este caso ambos casos serían igualmente difíciles de usar y un Stream sería una mejor opción ya que no es necesario cargar todos los elementos. Pero de nuevo no es tan común ... ¿o no?

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

Gracias

Respuesta

40

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

Tome este generador de números primos clásico, 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)) 

Puede ser fácilmente escrito con un Iterator también, pero no un Iteratormantendrá los números primos computados hasta ahora.

Entonces, 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 costosas computaciones/listas infinitas, estas cosas se pueden hacer con Iterator también. Las listas infinitas en realidad son bastante útiles; simplemente no lo sabe porque no lo tenía, por lo que ha visto algoritmos que son más complejos de lo estrictamente necesario para tratar con tamaños finitos forzados.

+2

Otra diferencia que me gustaría añadir es que 'Stream' nunca es floja en su elemento principal. La cabeza de un 'Stream' se almacena en forma evaluada. Si se necesita una secuencia donde ningún elemento (incluido el encabezado) se compute hasta que se solicite, 'Iterator' es la única opción. – Lii

+0

Además de la falta de holgazanería en el elemento principal, también evalúa cada elemento que desee eliminar. ej .: '" a "# ::" b "# ::" c "# ::" d "# :: Stream.empy [String] .drop (3)' evaluará "a", "b", " c "y" d ". "d" porque se convierte en cabeza. – r90t

+0

Interesante ejemplo sucinto para el generador de números primos.Curiosamente, si creo eso en una consola Scala simple y luego pregunto 4000 primos (no tanto en la práctica, tengo una definición alternativa que crea 100K en menos de 2 segundos) falla Scala con un error "Límite superior de GC rebasado" . –

17

Además de la respuesta de Daniel, tenga en cuenta que Stream es útil para las evaluaciones de cortocircuito.Por ejemplo, supongamos que tengo un enorme conjunto de funciones que toman String y vuelven Option[String], y quiero seguir la ejecución de ellos hasta que uno de ellos funciona:

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 el toda la lista, y no hay ningún método útil en List que diga, "tratar estas funciones como y ejecutarlas hasta que una de ellas devuelva algo que no sea None". ¿Qué hacer? Tal vez esto:

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

Esto toma una lista y lo trata como un Stream (que en realidad no evaluar nada), a continuación, define una nueva Stream que es el resultado de la aplicación de las funciones (pero eso no evalúa cualquier cosa todavía), luego busca el primero 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 desde Option[Option[String]] a Option[String] usando getOrElse.

He aquí 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 por lo que podemos decir si se les llama, 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 

(Esta es la Scala 2.8, la aplicación de 2,7 a veces sobrepasar por uno, por desgracia, y observe que. hacer acumular una larga lista de None como sus fracasos se acumulan, pero se supone que esto es barato en comparación con su verdadero cálculo aquí.)

+0

De hecho, soy un ejemplo, pero esto se puede hacer tan fácilmente con 'Iterator', así que decidí que no iba al caso. –

+2

concedido. Debería haber aclarado que esto no es específico de Stream, o escogí un ejemplo que usó varias llamadas al mismo Stream. –

2

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

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

Como señala Daniel, lo que favorece la pereza más de rigurosidad puede simplificar algoritmos y hacer más fácil para componer ellos.

+1

Por supuesto, para los nuevos en la pereza, la principal advertencia es que reduce la predictibilidad del código, puede conducir a heisenbugs y puede tener graves problemas de rendimiento para algunas clases de algoritmos. –

7

Me imagino que si sondeas algún dispositivo en tiempo real, un Stream es más conveniente.

Piense en un rastreador GPS, que devuelve la posición real si lo solicita. No puede precalcular la ubicación en la que estará en 5 minutos. Puede usarlo durante unos minutos solo para actualizar una ruta en OpenStreetMap o puede usarlo para una expedición de más de seis meses en un desierto o en la selva tropical.

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

+1

Votado hacia arriba para buen uso en caso de Stream. – nilskp

Cuestiones relacionadas