Numerables y Flujos (Enumerables, Streams)
Numerables
Elixir provee el concepto de numerables y el módulo Enum
para trabajar con ellos. Ya hemos visto dos tipos de numerables, listas y mapas.
Enum.map([1, 2, 3], fn x -> x * 2 end)
Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
El módulo Enum
provee un gran rango de funciones para transformar, ordenar, agrupar, filtrar y obtener ítems desde numerables. Es uno de los módulos frecuentemente usado por los desarrolladores en Elixir.
Elixir también provee rangos:
Enum.map(1..3, fn x -> x * 2 end)
Enum.reduce(1..3, 0, &+/2)
Las funciones en el módulo Enum
están limitadas a, como su nombre lo indica, enumerar valores en estructuras de datos. Para operaciones específicas, como insertar o actualizar algún elemento en particular, necesitas utilizar módulos específicos para dicho tipo de dato. Por ejemplo, si quieres insertar un elemento en una cierta posición en una lista, debes usar la función List.insert_at/3
disponible en el módulo List
, dado que no tendría mucho sentido insertar dicho valor en un rango, por ejemplo.
Decimos que las funciones en el módulo Enum
son polimorfas porque ellas pueden trabajar con distintos tipos de datos. En particular, las funciones en este módulo pueden trabajar con cualquier tipo de dato que implemente el protocolo Enumerable
. Discutiremos sobre Protocolos en clases posteriores; pero por ahora vamos a avanzar con un tipo específico de numerable llamado stream o flujo.
Evaluación temprana vs. Evaluación perezosa
Todas las funciones del módulo Enum
son ansiosas o ejecutan una evaluación temprana. Muchas funciones esperan un numerable y retornan una lista:
odd? = &(rem(&1, 2) != 0)
Enum.filter(1..3, odd?)
Esto significa que cuando estamos realizando múltiples operaciones con Enum
, cada operación va a generar una lista intermedia hasta que finalmente alcancemos el resultado:
1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum()
El ejemplo anterior tiene una secuencia de operaciones. Comenzamos con un rango y luego multiplicamos cada elemento en dicho rango por tres. La primera operación creará y retornará una lista con 100_000
elementos. Luego filtramos la lista previa y mantenemos solo los números impares, generando una nueva lista, ahora con 50_000
elementos, y finalmente sumamos todos los elementos.
El operador pipe (|>)
El símbolo |>
usado en el ejemplo previo es conocido como el operador pipe, dicho operador toma la salida de la expresión a su izquierda y la pasa como primer argumento a la función que es llamada a su lado derecho. Es similar al operador |
en Unix. Su propósito es resaltar que los datos están siendo transformados por una serie de funciones. Para ver cómo el operador |>
contribuye a generar código más limpio, vamos a reescribir el ejemplo anterior sin usar dicho operador.
Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
Puedes encontrar más información acerca del operador pipe leyendo su documentación.