Skip to main content

Colecciones Perezosas

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/collections-kt

Be lazy...

Puedes ejecutar el siguiente comando para crear el módulo

./gradlew setupLazyModule

Mientras se crean los archivos necesarios, puedes leer el código para saber qué está pasando.

import tasks.ModuleSetupTask

tasks.register<ModuleSetupTask>("setupLazyModule") {
description = "Creates the base module and files for the Lazy Collections module"
module.set("lazy")

doLast {

}
}

Preocúpate de que el plugin lazy esté aplicado en el archivo build.gradle.kts de tu proyecto.

./gradlew setupLazyModule

Preocúpate de que el nuevo módulo esté incluido en el archivo settings.gradle.kts.

En Kotlin, el uso de colecciones es habitual para transformar y procesar datos. Sin embargo, en ciertos escenarios —como flujos infinitos, operaciones costosas o pipelines complejos—, las colecciones tradicionales pueden introducir costos innecesarios en tiempo y memoria debido a su evaluación inmediata.

En esta lección abordaremos las colecciones perezosas, aquellas que difieren la evaluación de sus elementos hasta que realmente se necesitan. Veremos cómo este enfoque permite mejorar la eficiencia sin sacrificar expresividad, y cómo implementarlo tanto manualmente con Iterable e Iterator, como idiomáticamente con Sequence.

El objetivo es que puedas:

  • Entender cómo funciona la evaluación perezosa.
  • Diseñar estructuras perezosas personalizadas.
  • Comparar su comportamiento frente a colecciones tempranas.
  • Usarlas para escribir código más expresivo y eficiente en tus bibliotecas.

😴 ¿Qué es una Colección Perezosa?

Una colección perezosa es una estructura cuyos elementos no se calculan ni procesan de inmediato, sino que se generan solo cuando se necesitan. Esta estrategia es clave al diseñar bibliotecas que deben ser eficientes y componibles, especialmente cuando se trabaja con flujos infinitos, transformaciones encadenadas o datos costosos de calcular.

A diferencia de las colecciones tradicionales —que procesan todos los elementos tan pronto como se aplica una operación—, las colecciones perezosas permiten diferir la evaluación, lo que evita cálculos innecesarios y reduce el uso de memoria. Esto las hace ideales para construir APIs que necesitan ofrecer eficiencia sin sacrificar expresividad.

Evaluación temprana


La evaluación temprana es una estrategia en la cual las expresiones son evaluadas inmediatamente cuando el programa las encuentra. Esto garantiza que todos los valores estén disponibles cuando se necesiten, pero puede provocar cálculos inútiles o el uso excesivo de recursos si no todos los datos se van a utilizar.

Evaluación perezosa


La evaluación perezosa consiste en posponer el cálculo de una expresión hasta que su valor sea requerido. Al diferir la evaluación, se optimiza el uso de recursos y se evita trabajo innecesario. Esta técnica es especialmente útil en bibliotecas que manipulan flujos complejos, estructuras infinitas o secuencias derivadas de operaciones encadenadas.

💤 Implementación de una Colección Perezosa Personalizada

Al diseñar una biblioteca de utilidades numéricas, es común que deseemos ofrecer secuencias numéricas como parte de una API. Por ejemplo, podríamos querer incluir una función que permita generar una secuencia de números pares para tareas estadísticas, simulaciones o transformaciones funcionales.

En lugar de devolver una lista completa —lo cual podría ser ineficiente o incluso inviable si la secuencia es infinita—, una mejor opción es exponer una colección perezosa que genere estos valores bajo demanda. Esto no solo mejora el rendimiento, sino que permite componer operaciones de manera más expresiva y segura.

En este ejemplo, implementamos una colección perezosa como parte de una biblioteca que expone una secuencia infinita de números pares.

📋 Especificación BDD

"Given a lazy sequence of even numbers from the library" - {
"when the user requests the first n elements" - {
"then the sequence should produce the first n even numbers correctly" {}
}
}

📝 Implementación de las pruebas

checkAll(Arb.int(1..100)) { n ->
val evens = EvenNumbers()
val collected = mutableListOf<Int>()
val iterator = evens.iterator()

while (iterator.hasNext() && collected.size < n) {
collected += iterator.next()
}

val expected = (0..<(n * 2) step 2).toList()
collected shouldBe expected
}
¿Qué acabamos de hacer?

Esta prueba valida que la secuencia EvenNumbers() —provista por nuestra biblioteca— genera los primeros n números pares de forma perezosa y correcta.

  • Se usa Property-Based Testing para verificar el comportamiento con diferentes valores de n.
  • La colección resultante se compara con una lista construida manualmente usando (0..<(n * 2) step 2), que representa la especificación esperada de la secuencia de pares.
  • Este enfoque garantiza que el generador perezoso cumple con la interfaz Iterable y que su comportamiento es consistente y predecible, cualidades esenciales en componentes reutilizables dentro de una biblioteca.

🧩 Definiendo la clase EvenNumbers

package com.github.username.even

class EvenNumbers : Iterable<Int> {
override fun iterator(): Iterator<Int> = EvenNumberIterator()
}
¿Qué acabamos de hacer?
  • Creamos una clase EvenNumbers que representa una colección perezosa de números pares. Al implementar Iterable<Int>, esta clase se puede usar como cualquier colección de Kotlin en ciclos for, llamadas a toList(), o cualquier otra operación que recorra elementos.
  • En lugar de almacenar los números en memoria, delega la generación de valores a un iterador (EvenNumberIterator), lo que permite producir los números uno a uno, solo cuando se necesitan. Este diseño es útil para bibliotecas que necesitan exponer flujos infinitos o cálculos diferidos de manera limpia y reutilizable.

⚙️ Definiendo la clase EvenNumberIterator

package com.github.username.even

class EvenNumberIterator : Iterator<Int> {
private var current = 0

override fun hasNext(): Boolean = true

override fun next() = if (current >= Int.MAX_VALUE - 1) {
throw NoSuchElementException()
} else {
val nextValue = current
current += 2
nextValue
}
}
¿Qué acabamos de hacer?
  • Implementamos un iterador personalizado llamado EvenNumberIterator, que genera números pares de forma infinita y perezosa. Cada vez que se llama a next(), se produce el siguiente número par sin necesidad de precomputar la secuencia completa.
  • El método hasNext() siempre devuelve true porque el flujo es infinito, aunque protegemos el límite superior de Int para evitar desbordamientos. Este patrón permite a las bibliotecas ofrecer secuencias numéricas eficientes y seguras, ideales para flujos de datos, simulaciones o generadores en tiempo real.
Consideraciones al Usar Colecciones Perezosas
  • Control del Flujo: Es crucial limitar el número de elementos que se consumen de una secuencia infinita para evitar bucles infinitos y desbordamientos de memoria.
  • Inmutabilidad: Las colecciones perezosas suelen ser inmutables, promoviendo un estilo de programación funcional y evitando efectos secundarios.
  • Comprensión de la Evaluación Perezosa: Es importante entender cómo y cuándo se evalúan las operaciones para evitar comportamientos inesperados.

📊 Comparación entre Tipos de Colecciones en Kotlin

CaracterísticaEvaluación Temprana (Inmutables)
List, Set, Map
Evaluación Temprana Mutables
MutableList, MutableSet, MutableMap
Evaluación Perezosa
Sequence o personalizada
EvaluaciónInmediataInmediataDiferida (lazy)
MutabilidadInmutablesMutablesInmutables por defecto
Uso de memoriaAlto: genera colecciones intermediasAlto (similar a inmutables)Bajo: evita estructuras intermedias
ProcesamientoProcesa todos los elementos al aplicar una operaciónIgual que inmutables, pero con modificación directaSolo procesa lo que se necesita
Flujos infinitos❌ No soportado❌ No soportado✅ Soportado
Encadenamiento de operacionesMenos eficiente: cada paso crea una nueva colecciónSimilar, con posibilidad de modificarMás eficiente gracias a la evaluación perezosa
FlexibilidadFácil de usar y entenderMás flexible por ser modificableRequiere diseño cuidadoso
Ejemplo de usoDatos constantesListas dinámicas que cambian en tiempo de ejecuciónFlujos de datos grandes o infinitos
Mutaciones durante la iteración❌ No permitidas✅ Permitidas con MutableIteratorNo aplica directamente

😪 Colecciones perezosas en Kotlin: Sequence

En Kotlin, la interfaz Sequence representa una colección perezosa, diseñada para trabajar con grandes volúmenes de datos o flujos potencialmente infinitos sin generar colecciones intermedias en memoria.

Las secuencias permiten encadenar operaciones intermedias como map, filter o takeWhile sin que estas se evalúen inmediatamente. En su lugar, las transformaciones se aplican de forma diferida, una a una, y solo cuando se invoca una operación terminal —como toList(), first(), o forEach()—. Esto permite mejorar el rendimiento y reducir el uso de memoria, especialmente útil al diseñar APIs de procesamiento de datos o bibliotecas de transformación funcional.

💡 Ejemplo de uso de secuencias en Kotlin

Veamos cómo construir una secuencia infinita de números pares y obtener los primeros n elementos usando dos enfoques idiomáticos de Kotlin. Ambos ejemplos ilustran cómo la evaluación perezosa puede ayudarte a escribir código más eficiente y expresivo, especialmente útil al construir bibliotecas que procesan flujos de datos.

val evenNumbers = sequence {
var number = 0
while (true) {
yield(number)
number += 2
}
}

val firstNEvens = evenNumbers
.take(SIZE)
.toList()
¿Qué acabamos de hacer?
  • sequence {} permite definir una secuencia perezosa manualmente.
  • Dentro del bloque, generamos un flujo infinito de números pares usando yield.
  • Solo se generan los elementos necesarios gracias a take(SIZE), y luego se materializan en memoria con toList().

Este patrón te permite construir flujos infinitos o costosos de forma segura, sin incurrir en sobrecarga de memoria. Es una herramienta clave cuando diseñas bibliotecas que transforman datos en múltiples etapas sin crear estructuras intermedias innecesarias.

🧪 Ejercicio: Secuencia de Fibonacci

Ejercicio

Imagina que estás construyendo una biblioteca de secuencias numéricas. Tu objetivo es implementar una secuencia perezosa que genere los números de Fibonacci usando Sequence.

Cada número de Fibonacci se define como la suma de los dos anteriores, comenzando con 0 y 1.

No almacenes todos los elementos generados: deben calcularse bajo demanda.

Solución
fun fibonacciSequence(): Sequence<Long> = sequence {
var a = 0L
var b = 1L
while (true) {
yield(a)
a = b.also { b += a }
}
}

🎯 Conclusiones

El uso de colecciones perezosas representa una herramienta poderosa para quienes diseñan bibliotecas orientadas al procesamiento de datos, algoritmos numéricos o flujos potencialmente infinitos. A diferencia de las colecciones tradicionales, las colecciones perezosas permiten construir APIs más expresivas, eficientes y componibles, favoreciendo la claridad sin sacrificar rendimiento.

Durante esta lección, implementamos una colección perezosa personalizada utilizando el patrón Iterator, y exploramos cómo Kotlin provee construcciones idiomáticas como Sequence y generateSequence que permiten lograr el mismo objetivo de forma más declarativa y legible.

🔑 Puntos clave

  • Las colecciones tradicionales evalúan sus elementos de inmediato, lo que puede generar cálculos innecesarios o consumo de memoria excesivo.
  • Las colecciones perezosas difieren la evaluación hasta que los elementos son requeridos, optimizando recursos.
  • Kotlin ofrece soporte idiomático para este patrón a través de Sequence, sequence {} y generateSequence(...).
  • Al diseñar bibliotecas, este enfoque permite exponer flujos reutilizables, evitar estructuras temporales y mejorar la componibilidad de las APIs.

🧰 ¿Qué nos llevamos?

Diseñar colecciones perezosas no solo es una técnica para mejorar el rendimiento: es una forma de pensar en términos de eficiencia, claridad y componibilidad. Al adoptar este enfoque en nuestras bibliotecas, no solo optimizamos recursos, sino que también promovemos un diseño más expresivo y declarativo, donde las transformaciones ocurren solo cuando son necesarias.

Este modelo de evaluación nos invita a repensar cómo construimos APIs: en lugar de forzar al usuario a cargar y transformar datos prematuramente, le damos el control para decidir cuándo y cuánto necesita. Así, las colecciones perezosas se convierten en aliadas poderosas para escribir código reutilizable, predecible y elegante —especialmente cuando trabajamos con flujos infinitos, datos derivados o estructuras altamente dinámicas.

En definitiva, nos llevamos una herramienta conceptual y práctica que expande nuestra caja de herramientas como diseñadores de software, ayudándonos a construir bibliotecas más eficientes, expresivas y sostenibles.

📖 Referencias

🔥 Recomendadas

🔹 Adicionales