with

with permite agrupar múltiples expresiones de tipo pattern matching en un único bloque do-end, de tal manera que la única forma de evaluar su interior es que todos los matches sean válidos a la vez. Con esto podemos desplegar cómodamente expresiones envueltas en tuplas sin crear un excesivo número de cases.

La palabra clave with en Elixir es una herramienta poderosa para manejar errores y encadenar operaciones que devuelven resultados en forma de tuplas, como {:ok, valor} o {:error, motivo}. Nos permite escribir código más limpio y legible cuando trabajamos con funciones que pueden fallar, evitando anidamientos excesivos de case y facilitando el control de fallos.

Imaginemos que tenemos un archivo JSON con datos de empleados, donde cada empleado tiene un sueldo, y queremos sumar todos esos sueldos. Para ello, usamos la librería JSON de Elixir, que ofrece funciones como encode y decode para convertir entre estructuras nativas y cadenas JSON. Estas funciones devuelven siempre una tupla que indica si la operación fue exitosa o no, por ejemplo, {:ok, resultado} o {:error, razón}.

Un problema común es que no podemos encadenar directamente funciones como File.read y JSON.decode usando el operador pipeline (|>), porque la salida de una es una tupla y la siguiente espera un valor simple. Por ejemplo, File.read("empleados.json") devuelve {:ok, contenido} o {:error, razón}, y no podemos pasar esa tupla directamente a JSON.decode.

Una forma tradicional de manejar esto es usar múltiples bloques case para ir comprobando cada paso, pero esto puede hacer que el código se vuelva muy anidado y difícil de seguir. Por ejemplo, primero hacemos un case para leer el archivo, luego otro para decodificar el JSON, y otro más para extraer los datos que necesitamos.

Aquí es donde entra en juego with. Esta palabra clave nos permite encadenar varios patrones de coincidencia (matcheos) de forma secuencial. Si todos los patrones coinciden, ejecutamos el bloque principal; si alguno falla, podemos manejar el error en un bloque else separado.

Veamos cómo sería el código para leer el archivo, decodificar el JSON y extraer la lista de empleados usando with:

with {:ok, file} <- File.read("empleados.json"),
     {:ok, data} <- JSON.decode(file),
     {:ok, empleados} <- Map.fetch(data, "empleados") do
  empleados
  |> Enum.map(& &1["sueldo"])
  |> Enum.sum()
else
  {:error, reason} -> {:error, reason}
  _ -> {:error, :unknown_error}
end

En este fragmento, cada línea dentro del with intenta hacer un patrón de coincidencia con el resultado de una función. Por ejemplo, {:ok, file} <- File.read("empleados.json") intenta extraer el contenido del archivo si la lectura fue exitosa. Si alguna de estas líneas no coincide (por ejemplo, si File.read devuelve un error), el flujo salta al bloque else, donde podemos manejar el error de forma centralizada.

Dentro del bloque do de with, ya podemos trabajar con las variables extraídas con total seguridad, porque sabemos que todas las operaciones anteriores han sido exitosas. En nuestro caso, extraemos los sueldos de cada empleado y los sumamos.

Este enfoque nos permite escribir código que se lee casi como una secuencia lineal de pasos, sin tener que anidar múltiples case o manejar errores en cada paso por separado. Además, el bloque else nos da un lugar único para tratar todos los posibles errores, pudiendo hacer un patrón de coincidencia más específico si queremos distinguir entre distintos tipos de fallos, como errores de lectura de archivo, errores de decodificación JSON o claves faltantes en el mapa.

Es importante destacar que with no es simplemente un azúcar sintáctico para case, sino que facilita el manejo de flujos donde cada paso depende del éxito del anterior, especialmente cuando trabajamos con estructuras como tuplas {:ok, valor} y {:error, razón} que son comunes en Elixir.

En resumen, with nos ayuda a escribir código más limpio y mantenible cuando tenemos que encadenar operaciones que pueden fallar, como leer archivos, decodificar JSON y extraer datos, evitando la complejidad de múltiples anidamientos y centralizando el manejo de errores. Esto es especialmente útil en Elixir, donde el patrón de retorno con tuplas es habitual y el control explícito de errores es una práctica recomendada.

Lista de reproducción
  1. 1
    mix
    10 minutos
  2. 2
    Documentando código: comentarios, docs y moduledocs
    10 minutos
  3. 3
    Atributos de módulo
    9 minutos
  4. 4
    Dependencias
    12 minutos
  5. 5
    Un ejemplo práctico de módulo útil
    13 minutos
  6. 6
    Alias e import
    10 minutos
  7. 7
    Sobre las macros, require y use
    11 minutos
  8. 8
    Typespecs (parte 1, usando tipos básicos)
    10 minutos
  9. 9
    Typespecs (parte 2, tipos propios y t())
    11 minutos
  10. 10
    Comportamientos
    11 minutos
  11. 11
    Tratamiento de errores con rescue
    8 minutos
  12. 12
    Elevando errores con raise
    8 minutos
  13. 13
    with
    14 minutos
  14. 14
    Sigilos
    8 minutos
  15. 15
    Tests con ExUnit
    12 minutos
  16. 16
    Más particularidades de ExUnit
    13 minutos
  17. 17
    Microservicios en Elixir con Plug
    11 minutos
  18. 18
    Cómo Plug.Router te ayuda a escribir microservicios en Elixir
    14 minutos
  19. 19
    ¿Cómo hacer rutas dinámicas en Phoenix y Plug?
    13 minutos