Cuando trabajamos con listas en Elixir, combinar funciones como filter, map y reduce nos permite hacer transformaciones potentes, pero a veces el código puede volverse difícil de leer y entender. Por ejemplo, imaginemos que tenemos una lista con los números del 1 al 9 y queremos obtener el doble de aquellos elementos que sean múltiplos de 2 o de 3, para luego sumar esos valores. Si intentamos encadenar estas funciones sin ninguna ayuda, el código puede resultar confuso y complicado de seguir, especialmente cuando anidamos llamadas como filter(map(lista, ...), ...) o reduce(filter(map(lista, ...), ...), ...).
Para resolver este problema, Elixir nos ofrece un operador muy útil llamado pipeline, que se escribe como |>. Este operador nos permite encadenar funciones de manera clara y ordenada, pasando el resultado de una expresión como primer parámetro de la siguiente función. Por ejemplo, en lugar de escribir:
reduce(
map(
filter(lista, fn x -> rem(x, 2) == 0 or rem(x, 3) == 0 end),
fn x -> x * 2 end
),
0,
fn x, acc -> x + acc end
)
Podemos usar el pipeline para hacerlo mucho más legible:
lista
|> filter(fn x -> rem(x, 2) == 0 or rem(x, 3) == 0 end)
|> map(fn x -> x * 2 end)
|> reduce(0, fn x, acc -> x + acc end)
Aquí, el operador |> toma el valor de la izquierda y lo pasa como primer argumento a la función de la derecha. Si la función requiere más parámetros, los añadimos después. Esto hace que el código se lea como una cadena de transformaciones, donde cada paso recibe el resultado del anterior.
Además, podemos mejorar la legibilidad poniendo cada llamada en una línea separada, lo que facilita entender el flujo de datos y las transformaciones que aplicamos. Por ejemplo:
lista
|> filter(fn x -> rem(x, 2) == 0 or rem(x, 3) == 0 end)
|> map(fn x -> x * 2 end)
|> reduce(0, fn x, acc -> x + acc end)
Otra ventaja del pipeline es que podemos insertar inspecciones intermedias para ver cómo va cambiando el dato a lo largo de la cadena. Usando la función io.inspect/1, que imprime el valor recibido y lo devuelve sin modificarlo, podemos colocarla en cualquier punto del pipeline para depurar o entender mejor el proceso:
lista
|> filter(fn x -> rem(x, 2) == 0 or rem(x, 3) == 0 end)
|> io.inspect()
|> map(fn x -> x * 2 end)
|> io.inspect()
|> reduce(0, fn x, acc -> x + acc end)
Esto nos muestra el estado de la lista después de filtrar y después de mapear, sin interrumpir la cadena de transformaciones.
Es importante destacar que para que el pipeline funcione correctamente, las funciones que encadenamos deben tener como primer parámetro el dato que queremos transformar. Por eso, muchas funciones en Elixir están diseñadas con esta convención. Por ejemplo, Map.put/3 recibe primero el mapa, luego la clave y el valor; Map.keys/1 recibe el mapa como primer parámetro. Esto facilita que podamos encadenar llamadas con el operador pipeline sin complicaciones.
Veamos un ejemplo práctico con mapas:
%{}
|> Map.put(:a, 1)
|> Map.put(:b, 2)
|> Map.keys()
|> io.inspect()
Aquí empezamos con un mapa vacío, le añadimos dos claves con sus valores y luego obtenemos las claves, que finalmente imprimimos. Todo esto en una cadena clara y fácil de seguir.
Cuando escribimos código en archivos, podemos aprovechar la flexibilidad del pipeline para organizarlo con indentaciones y saltos de línea que mejoren la legibilidad. Por ejemplo:
lista
|> io.inspect(label: "Lista original")
|> map(fn x -> x + 2 end)
|> filter(fn x -> rem(x, 2) == 0 end)
|> io.inspect(label: "Después de map y filter")
Así, podemos ver claramente cada paso y entender cómo se transforma la información.
El uso del operador pipeline es especialmente común cuando trabajamos con librerías como Plug o Ecto, donde las transformaciones encadenadas son la norma. En estos contextos, el pipeline nos ayuda a escribir código más limpio y mantenible, evitando la confusión que puede generar el anidamiento excesivo de funciones.
En definitiva, el operador pipeline nos permite construir cadenas de transformaciones tan largas como necesitemos, manteniendo la claridad y facilitando la comprensión del flujo de datos en nuestros programas Elixir. Esto nos da mucha más soltura para escribir código complejo sin perdernos en paréntesis o llamadas anidadas.