Una de las primeras cosas que descoloca a quien llega a Elixir desde Python, JavaScript o Java es que el operador = no asigna un valor a una variable. Hace match. Esa diferencia cambia la forma de leer y escribir código, y una vez que te acostumbras, resulta difícil volver a los patrones imperativos de antes.
El operador de match
En Elixir, = es el operador de match. Lo que hace es intentar hacer que el lado izquierdo coincida con el lado derecho. Si puede, liga las variables que encuentre. Si no puede, lanza un error.
# Esto funciona: x queda ligada a 42
x = 42
# Esto también: desestructura la tupla
{:ok, resultado} = {:ok, "datos"}
# resultado = "datos"
# Esto falla en tiempo de ejecución:
{:ok, resultado} = {:error, "algo falló"}
# ** (MatchError) no match of right hand side value: {:error, "algo falló"}
# El pin operator ^ fuerza comparar, no ligar
x = 1
^x = 1 # ok
^x = 2 # MatchError: x era 1, no 2
El fallo inmediato es intencionado. En lugar de continuar con un estado inesperado, el proceso falla y (si hay un supervisor) se reinicia. La filosofía de OTP de "deja fallar" se apoya exactamente en esto.
Desestructuración de listas, mapas y structs
El pattern matching funciona sobre cualquier tipo de datos:
# Listas
[primero | resto] = [1, 2, 3]
# primero = 1, resto = [2, 3]
[a, b, c] = [10, 20, 30]
# a=10, b=20, c=30
# Mapas (solo necesitas especificar las claves que te interesan)
%{nombre: nombre, edad: edad} = %{nombre: "Ana", edad: 30, ciudad: "Madrid"}
# nombre = "Ana", edad = 30 (ciudad se ignora)
# Structs
%Usuario{nombre: nombre, email: email} = usuario
# Extrae nombre y email del struct Usuario
Cláusulas de función: el pattern matching aplicado
Donde el pattern matching brilla de verdad es en las cláusulas de función. En lugar de un if-else anidado dentro de la función, defines varias versiones de la misma función para distintos patrones:
defmodule Calculadora do
# Caso base: lista vacía
def suma([]), do: 0
# Caso recursivo: desestructura el primer elemento
def suma([cabeza | cola]) do
cabeza + suma(cola)
end
end
Calculadora.suma([1, 2, 3, 4]) # 10
Elixir prueba las cláusulas en orden de definición y usa la primera que hace match. Esto sustituye a los switch/case de otros lenguajes con algo mucho más expresivo.
defmodule Respuesta do
def procesar({:ok, datos}) do
IO.puts("Éxito: #{inspect(datos)}")
end
def procesar({:error, :not_found}) do
IO.puts("No encontrado")
end
def procesar({:error, razon}) do
IO.puts("Error: #{inspect(razon)}")
end
end
Guards: condiciones adicionales sobre los patrones
Los guards añaden condiciones que no se pueden expresar solo con la estructura:
defmodule Clasificador do
def clasificar(n) when is_integer(n) and n > 0, do: :positivo
def clasificar(n) when is_integer(n) and n < 0, do: :negativo
def clasificar(0), do: :cero
def clasificar(n) when is_float(n), do: :decimal
def clasificar(_), do: :otro
end
Clasificador.clasificar(42) # :positivo
Clasificador.clasificar(-5) # :negativo
Clasificador.clasificar(0) # :cero
Clasificador.clasificar(3.14) # :decimal
Clasificador.clasificar("x") # :otro
Solo puedes usar funciones puras en los guards (las que empieza por is_, operadores aritméticos, length/1, elem/2, etc.). No puedes llamar a funciones con efectos secundarios.
case, cond y with
# case: match contra un valor
case Map.get(mapa, :clave) do
nil -> "no existe"
valor when is_binary(valor) -> "texto: #{valor}"
valor -> "otro: #{inspect(valor)}"
end
# cond: como un if-elsif encadenado
cond do
edad < 18 -> "menor"
edad < 65 -> "adulto"
true -> "senior"
end
# with: encadenamiento de matches que puede fallar
with {:ok, usuario} <- Repo.get_usuario(id),
{:ok, _} <- Autorizacion.verificar(usuario),
{:ok, datos} <- Servicio.obtener_datos(usuario) do
procesar(datos)
else
{:error, :not_found} -> {:error, "usuario no existe"}
{:error, :no_autorizado} -> {:error, "sin permiso"}
{:error, razon} -> {:error, "fallo: #{inspect(razon)}"}
end
with es especialmente útil para flujos donde cada paso puede fallar. Si algún match falla, salta al bloque else con el valor que no hizo match. Sin with, tendrías que anidar varios case que crecen hacia la derecha.
Pattern matching en los datos de Ecto y Phoenix
En la práctica, el pattern matching está en todas partes dentro de un proyecto Elixir. En los controllers de Phoenix:
def create(conn, %{"usuario" => params}) do
case Cuentas.crear_usuario(params) do
{:ok, usuario} ->
conn
|> put_status(:created)
|> render(:show, usuario: usuario)
{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(:errors, changeset: changeset)
end
end
La desestructuración de %{"usuario" => params} en los parámetros de la función extrae directamente lo que necesitas de los parámetros HTTP. No hay params["usuario"] más abajo, ya tienes lo que quieres.
Una vez que interiorizas este modelo, leer código Elixir se vuelve más fácil porque el flujo de datos está visible en la estructura del código. Para ver cómo este enfoque complementa la concurrencia con OTP, el artículo sobre GenServer de esta serie muestra cómo el pattern matching se aplica en los callbacks de los procesos. También merece la pena compararlo con el sistema de tipos de Rust, donde el match exhaustivo sobre enums es obligatorio.
Imagen: Pexels / Myburgh Roux
