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
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
- Código esencial
- Código completo
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
}
package com.github.username.numeric
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.checkAll
class NumericSequencesTest : FreeSpec({
"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" {
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
}
}
}
}
})
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()
}
- Creamos una clase
EvenNumbers
que representa una colección perezosa de números pares. Al implementarIterable<Int>
, esta clase se puede usar como cualquier colección de Kotlin en ciclosfor
, llamadas atoList()
, 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
}
}
- Implementamos un iterador personalizado llamado
EvenNumberIterator
, que genera números pares de forma infinita y perezosa. Cada vez que se llama anext()
, se produce el siguiente número par sin necesidad de precomputar la secuencia completa. - El método
hasNext()
siempre devuelvetrue
porque el flujo es infinito, aunque protegemos el límite superior deInt
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.
- 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ística | Evaluación Temprana (Inmutables)List , Set , Map | Evaluación Temprana MutablesMutableList , MutableSet , MutableMap | Evaluación PerezosaSequence o personalizada |
---|---|---|---|
Evaluación | Inmediata | Inmediata | Diferida (lazy) |
Mutabilidad | Inmutables | Mutables | Inmutables por defecto |
Uso de memoria | Alto: genera colecciones intermedias | Alto (similar a inmutables) | Bajo: evita estructuras intermedias |
Procesamiento | Procesa todos los elementos al aplicar una operación | Igual que inmutables, pero con modificación directa | Solo procesa lo que se necesita |
Flujos infinitos | ❌ No soportado | ❌ No soportado | ✅ Soportado |
Encadenamiento de operaciones | Menos eficiente: cada paso crea una nueva colección | Similar, con posibilidad de modificar | Más eficiente gracias a la evaluación perezosa |
Flexibilidad | Fácil de usar y entender | Más flexible por ser modificable | Requiere diseño cuidadoso |
Ejemplo de uso | Datos constantes | Listas dinámicas que cambian en tiempo de ejecución | Flujos de datos grandes o infinitos |
Mutaciones durante la iteración | ❌ No permitidas | ✅ Permitidas con MutableIterator | No 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.
- Implementación básica
- Implementación mejorada
val evenNumbers = sequence {
var number = 0
while (true) {
yield(number)
number += 2
}
}
val firstNEvens = evenNumbers
.take(SIZE)
.toList()
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 contoList()
.
val evenNumbers = generateSequence(0) { it + 2 }
val firstNEvens = evenNumbers
.take(SIZE)
.toList()
generateSequence(start) { next }
es una forma más concisa de definir secuencias basadas en funciones.- Es ideal para bibliotecas, ya que reduce el ruido y mejora la legibilidad sin sacrificar pereza ni expresividad.
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 {}
ygenerateSequence(...)
. - 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
- 🌐 Tema 10: Evaluación perezosa. (s. f.). Recuperado 29 de marzo de 2025, de https://www.cs.us.es/~jalonso/cursos/i1m/temas/tema-10.html
🔹 Adicionales
- 📰 Casero, A. (2024, marzo 15). ¿Qué es la evaluación perezosa en programación? Keep Coding. https://keepcoding.io/blog/evaluacion-perezosa-en-programacion/