Entre las colecciones de Kotlin existen unas muy peculiares: las secuencias.

Las secuencias se diferencian del resto de colecciones en que, en vez de contener una serie de objetos ya disponibles desde el principio, esos objetos no se calculan hasta que no llega el momento de ser utilizados.

¿Y porqué te hablo de las secuencias de Kotlin ahora? Pues porque nos van a allanar mucho el terreno para entender los Flows, siguiendo con el tema de la programacion reactiva.

Entendiendo las secuencias nos va a costar muy poco el paso a Flows. Pero es que además las secuencias por sí mismas son muy interesantes.

Aquí vamos a entrar en conceptos un poco avanzados del lenguaje, así que si quieres mejorar tus habilidades como desarrollador Kotlin, te recomiendo que te apuntes a mi masterclass gratuita, donde te contaré cómo puedes dominar el lenguaje

Secuencias: cuándo usarlas

Como comentaba, la ventaja de una secuencia es que no tiene desde el primer minuto todos los objetos calculados, sino que se van creando según se necesitan. Esto nos trae dos ventajas:

  • Las secuencias pueden ser infinitas: podemos definir una secuencia mediante un valor inicial y una operación (por ejemplo), y esa secuencia tendrá infinitos valores
  • Permiten evitar pasos intermedios: a diferencia del resto de colecciones, cuando una secuencia realiza varias operaciones (de filtrado, transformación, etc), estas se aplican en cadena en los objetos uno a uno, en vez de necesitar crear una colección nueva a cada paso.

Son realmente muy potentes, así que vamos a ver más a fondo cómo usarlas

Cómo crear secuencias

Existen muchas formas de crear secuencias, desde algunas más sencillas con valores predeterminados, hasta otras mucho más dinámicas.

De un listado de elementos

Al igual que con el resto de colecciones, existe una función que nos permite crear secuencias con valores predefindos:

val sequence = sequenceOf("One", "Two", "Three", "Four")

Desde otra colección

Mediante la función asSequence(), podemos convertir cualquier colección en una secuencia.

val strNumbers = listOf("One", "Two", "Three", "Four")
val strNumbersSeq = list.asSequence()

Esto nos puede venir genial en el caso de que queramos hacer varias operaciones seguidas sobre una colección, para evitar que se creen colecciones intermedias.

Por ejemplo, podríamos hacer:

val strNumberSize = strNumbers
    .asSequence()
    .filter { it.length > 3 }
    .map { it.length }
    .toList()

En este caso, en vez de crear una colección con los elementos filtrados, y otra con los elementos mapeados, directamente la secuecia se guarda las operaciones, y cuando se llama a toList(), esas operaciones se computan y se genera la lista final desde la original.

Esto no siempre es lo óptimo, sobre todo en casos tan sencillos, pero si vemos que tenemos colecciones grandes con muchas operaciones, merece la pena medir y ver qué solución es más rápida.

Mediante una función

Existe una función llamada generateSequence(), que recibe un primer valor (semilla) y una función a aplicar sobre ese valor.

val oddNumbers = generateSequence(1) { it + 1 }

Existen variantes de esta función sin semilla, o con una semilla que es una función en vez de un valor concreto.

¿Y para qué quiero una secuencia infinita? En general, efectivamente, no tiene sentido, y lo que querremos hacer es en algún punto «cortar» esa generación de valores, por ejemplo:

        val result = oddNumbers
            .filter { it % 3 != 0 }
            .map { it.toString() }
            .takeWhile { it.length < 3 }
            .toList()

En el caso anterior se recuperarían valores hasta que la conversión a «toString()», de una longitud = 3. Es decir, cortaría en en la representación en String del número 100.

Luego se convierte a una lista que ya se puede usar para otra cosa.

Mediante grupos de valores

Existe una última opción que nos permite generar secuencia de una forma mucho más arbitrarias. Mediante un bloque sequence y una función yield, podemos generar valores en cualquier punto del código de esa función.

Quédate con esto, porque nos va a venir muy bien para los Flows:

val randomNumbers = sequence {
    yield(3)
    yieldAll(listOf(4, 5, 6, 8, 24))
    yieldAll(generateSequence(2) { it * it })
}
// 3, 4, 5, 6, 8, 24, 2, 4, 16, 256...

Tipos de operaciones sobre secuencias

Te puedes estar preguntando si todas las operaciones que podemos utilizar sobre una secuencia son iguales.

¿Con todas podemos conseguir el efecto de que no sea necesaria una colección intermedia? Pues la verdad es que no, y hay que tener un poco de cuidado con esto, porque nos podemos llevar la falsa sensación de que nuestro código es muy óptimo, y puede no ser así.

En función de esto, tenemos dos tipos de operaciones:

  • Stateless: no necesitan ningún estado intermedio para procesarse, o una cantidad muy pequeña y constante. Algunas operaciones stateless son map(), filter(), take() o drop()
  • Stateful: operaciones que requieren una gran cantidad de estado, normalmente proporcional al número de elementos de la secuencia. Algunos ejemplos serían todas las variantes de sorted(), distinct() o chunked()

También tenemos dos tipos de operaciones, en función de si estas generan una nueva secuencia o ya necesitan calcular el resultado:

  • Intermediate: al aplicarlo, el resultado devuelto es otra secuencia, y por tanto no necesita aún calcular el resultado a partir de los valores de la secuencia. Todas las operaciones de las que hablábamos antes son intermedias.
  • Terminal: necesita los valores de la secuencia, y por tanto, va a procesar toda la secuencia para obtener el resultado. Algunos ejemplos serían toList(), que devuelve una lista concreta, o sum() que calcula la suma de los valores de la secuencia.

Para saber de qué tipo es cada operación, lo más sencillo es hacer ctrl+click o cmd+click, y acceder al código fuente. Allí se indica en la documentación de la operación. También podrías ir a la documentación online para verlo.

Conclusión

Como ves, las secuencias son una funcionalidad muy potente que nos permite generar series de valores y transformarlos, todo ello sin necesidad de procesar los resultados hasta que realmente los necesitemos.

Esto nos puede ayudar a representar ciertas series de valores de forma mucho más sencilla, pero también a optimizar las operaciones que realizamos sobre colecciones.

Todo ello sin perder el objetivo de aprender Flow. Cuando hablemos de ello, verás que los flows no son más que secuencias asíncronas…

Mind Blown GIFs - Find & Share on GIPHY

Y recuerda que si quieres dominar Kotlin, tienes disponible mi masterclass gratuita donde te hablo de cómo ir al siguiente nivel, y mucho más.

Author: Antonio Leiva

Soy un apasionado de Kotlin. Hace ya más de dos años que estudio el lenguaje y su aplicación a Android para ayudarte a ti a aprenderlo de la forma más sencilla posible.