En Elixir, cuando necesitamos trabajar con listas de números consecutivos, los rangos se convierten en una herramienta fundamental que nos ahorra mucho trabajo. En lugar de escribir manualmente una lista como [1, 2, 3, 4, 5], podemos definir un rango con una sintaxis muy sencilla: simplemente ponemos el número inicial, seguido de dos puntos .., y luego el número final. Por ejemplo, 1..15 representa todos los números del 1 al 15 de forma automática y eficiente.
Este rango no es simplemente una lista, sino una estructura especial que Elixir maneja internamente. Podemos usarla directamente con funciones del módulo Enum, como Enum.map, Enum.sum o Enum.filter, para procesar los números que contiene. Por ejemplo, si queremos sumar todos los números del 1 al 15, basta con hacer:
Enum.sum(1..15)
Y Elixir nos devolverá el resultado sin necesidad de construir la lista manualmente. También podemos filtrar los números pares con:
Enum.filter(1..15, fn x -> rem(x, 2) == 0 end)
Esto nos devuelve solo los números pares dentro del rango.
Sin embargo, hay un detalle importante sobre cómo funcionan estas funciones de Enum. Son colecciones eager, es decir, que procesan todos los elementos de la colección antes de pasar al siguiente paso. Por ejemplo, si hacemos dos Enum.map consecutivos con una función que imprime cada elemento, veremos que primero se procesan todos los elementos del primer map y luego todos los del segundo. Esto significa que si tenemos una lista muy grande, como un rango de millones de números, el procesamiento completo puede ser muy costoso y lento.
Para ilustrar esto, imaginemos que queremos encontrar el primer número par en un rango muy grande usando Enum.find. Aunque el número que buscamos esté cerca del principio, Enum.find tendrá que procesar toda la colección antes de devolver el resultado, porque las funciones eager no liberan resultados parciales hasta completar todo el procesamiento.
Aquí es donde entran en juego las colecciones lazy, o vagas, que en Elixir se manejan con Stream. Un Stream no procesa los elementos inmediatamente, sino que espera hasta que sea estrictamente necesario. Por ejemplo, si usamos Stream.map sobre un rango, no se ejecuta ninguna transformación hasta que convertimos el stream en una lista o usamos una función que consume elementos, como Enum.take.
Veamos un ejemplo:
stream = 1..100 |> Stream.map(fn x -> IO.inspect(x); x * 2 end)
Enum.take(stream, 5)
En este caso, solo se procesan los primeros cinco elementos del rango, y el resto queda sin evaluar. Esto es muy eficiente cuando trabajamos con colecciones muy grandes y solo necesitamos una parte de los datos o queremos evitar cálculos innecesarios.
Las funciones de Stream como map y filter devuelven streams que se pueden encadenar sin que se ejecute el procesamiento hasta que se consume el stream. Esto nos permite construir pipelines complejas que solo se ejecutan cuando realmente necesitamos los resultados, ahorrando tiempo y recursos.
En resumen, los rangos en Elixir nos facilitan la creación de listas de números consecutivos, y combinados con Enum podemos procesarlos fácilmente. Pero cuando trabajamos con grandes cantidades de datos o queremos optimizar el rendimiento, usar Stream para crear colecciones lazy es una estrategia muy poderosa que nos permite procesar solo lo necesario y mejorar la eficiencia de nuestros programas.