Iterable y Sequence de Kotlin se ven exactamente iguales. ¿Por qué se requieren dos tipos?

86

Ambas interfaces definen solo un método

public operator fun iterator(): Iterator<T>

La documentación dice que Sequenceestá destinado a ser vago. ¿Pero no es Iterableperezoso también (a menos que esté respaldado por una Collection)?

Venkata Raju
fuente

Respuestas:

136

La diferencia clave radica en la semántica y la implementación de las funciones de extensión stdlib para Iterable<T>y Sequence<T>.

  • Porque Sequence<T>, las funciones de extensión funcionan de manera perezosa cuando es posible, de manera similar a las operaciones intermedias de Java Streams . Por ejemplo, Sequence<T>.map { ... }devuelve otro Sequence<R>y en realidad no procesa los elementos hasta que se llama a una operación de terminal como toListo fold.

    Considere este código:

    val seq = sequenceOf(1, 2)
    val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
    print("before sum ")
    val sum = seqMapped.sum() // terminal
    

    Imprime:

    before sum 1 2
    

    Sequence<T>está diseñado para un uso diferido y una canalización eficiente cuando desea reducir el trabajo realizado en las operaciones de la terminal tanto como sea posible, al igual que Java Streams. Sin embargo, la pereza introduce algunos gastos generales, que no son deseables para las transformaciones simples comunes de colecciones más pequeñas y las hace menos eficaces.

    En general, no hay una buena manera de determinar cuándo se necesita, por lo que en Kotlin stdlib la pereza se hace explícita y se extrae a la Sequence<T>interfaz para evitar usarla en todos los Iterables por defecto.

  • Porque Iterable<T>, por el contrario, las funciones de extensión con semántica de operación intermedia trabajan con entusiasmo, procesan los elementos de inmediato y devuelven otro Iterable. Por ejemplo, Iterable<T>.map { ... }devuelve a List<R>con los resultados de la asignación.

    El código equivalente para Iterable:

    val lst = listOf(1, 2)
    val lstMapped: List<Int> = lst.map { print("$it "); it * it }
    print("before sum ")
    val sum = lstMapped.sum()
    

    Esto imprime:

    1 2 before sum
    

    Como se dijo anteriormente, Iterable<T>no es perezoso de forma predeterminada, y esta solución se muestra bien: en la mayoría de los casos, tiene una buena localidad de referencia , aprovechando así el caché de la CPU, la predicción, la búsqueda previa, etc., de modo que incluso la copia múltiple de una colección sigue funcionando bien. suficiente y funciona mejor en casos simples con colecciones pequeñas.

    Si necesita más control sobre la canalización de evaluación, hay una conversión explícita a una secuencia perezosa con Iterable<T>.asSequence()función.

tecla de acceso rápido
fuente
3
Probablemente una gran sorpresa para los fanáticos Java(en su mayoría Guava)
Venkata Raju
@VenkataRaju para las personas funcionales, podrían sorprenderse con la alternativa de lazy por defecto.
Jayson Minard
9
Lazy por defecto suele ser menos eficaz para colecciones más pequeñas y de uso más común. Una copia puede ser más rápida que una evaluación perezosa si se aprovecha la memoria caché de la CPU, etc. Entonces, para casos de uso común, es mejor no ser perezoso. Y, desafortunadamente, los contratos comunes para funciones como map, filtery otras no contienen suficiente información para decidir más allá del tipo de colección de origen, y dado que la mayoría de las colecciones también son Iterables, ese no es un buen marcador para "ser perezoso" porque es comúnmente EN TODAS PARTES. perezoso debe ser explícito para estar seguro.
Jayson Minard
1
@naki Un ejemplo de un anuncio reciente de Apache Spark, están preocupados por esto obviamente, consulte la sección "Computación con memoria caché" en databricks.com/blog/2015/04/28/… ... pero están preocupados por miles de millones de cosas que se repiten, por lo que deben llegar al extremo.
Jayson Minard
3
Además, un error común con la evaluación perezosa es capturar el contexto y almacenar el cálculo perezoso resultante en un campo junto con todos los locales capturados y lo que sea que tengan. Por lo tanto, las fugas de memoria son difíciles de depurar.
Ilya Ryzhenkov
49

Completando la respuesta de la tecla de acceso rápido:

Es importante notar cómo Sequence e Iterable iteran a través de sus elementos:

Ejemplo de secuencia:

list.asSequence().filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

Resultado del registro:

filtro - Mapa - Cada uno; filtro - Mapa - Cada

Ejemplo iterable:

list.filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

filtro - filtro - Mapa - Mapa - Cada uno - Cada

Leandro Borges Ferreira
fuente
5
Ese es un excelente ejemplo de la diferencia entre los dos.
Alexey Soshin
Este es un gran ejemplo.
frye3k
2

Iterablese asigna a la java.lang.Iterableinterfaz en el JVM, y se implementa mediante colecciones de uso común, como List o Set. Las funciones de extensión de la colección en estos se evalúan con entusiasmo, lo que significa que todos procesan inmediatamente todos los elementos en su entrada y devuelven una nueva colección que contiene el resultado.

Aquí hay un ejemplo simple del uso de las funciones de colección para obtener los nombres de las primeras cinco personas en una lista cuya edad es al menos 21:

val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)

Plataforma de destino: JVMRunning en kotlin v. 1.3.61 Primero, la verificación de edad se realiza para cada persona en la lista, con el resultado en una lista nueva. Luego, el mapeo a sus nombres se realiza para cada Persona que permaneció después del operador de filtro, terminando en otra lista nueva (ahora es a List<String>). Finalmente, hay una última lista nueva creada para contener los primeros cinco elementos de la lista anterior.

En contraste, Sequence es un concepto nuevo en Kotlin para representar una colección de valores evaluados de forma perezosa. Las mismas extensiones de colección están disponibles para la Sequenceinterfaz, pero estas devuelven inmediatamente instancias de Secuencia que representan un estado procesado de la fecha, pero sin procesar ningún elemento. Para comenzar a procesar, el Sequencetiene que ser terminado con un operador de terminal, estos son básicamente una solicitud a la Secuencia para materializar los datos que representa en alguna forma concreta. Los ejemplos incluyen toList, toSety sum, por mencionar solo algunos. Cuando se llaman, solo se procesará el número mínimo requerido de elementos para producir el resultado exigido.

Transformar una colección existente en una secuencia es bastante sencillo, solo necesita usar la asSequenceextensión. Como se mencionó anteriormente, también debe agregar un operador de terminal; de lo contrario, la secuencia nunca realizará ningún procesamiento (nuevamente, ¡perezoso!).

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)
    .toList()

Plataforma de destino: JVMRunning en kotlin v. 1.3.61 En este caso, cada una de las instancias de Person en la Secuencia se verifica por su edad, si pasan, se extrae su nombre y luego se agrega a la lista de resultados. Esto se repite para cada persona en la lista original hasta que se encuentran cinco personas. En este punto, la función toList devuelve una lista y el resto de las personas en el Sequenceno se procesan.

También hay algo extra de lo que es capaz una secuencia: puede contener una cantidad infinita de elementos. Con esto en perspectiva, tiene sentido que los operadores trabajen de la manera que lo hacen: un operador en una secuencia infinita nunca podría regresar si hiciera su trabajo con entusiasmo.

Como ejemplo, aquí hay una secuencia que generará tantas potencias de 2 como requiera su operador de terminal (ignorando el hecho de que esto se desbordaría rápidamente):

generateSequence(1) { n -> n * 2 }
    .take(20)
    .forEach(::println)

Puedes encontrar más aquí .

Sazzad Hissain Khan
fuente