Pattern Matching
Domina la herramienta más poderosa de Elixir para descomponer datos y controlar flujo. ⏱️ 2.5 horas
El operador = no es asignación
En Elixir, = es el operador de match,
no de asignación. La expresión a = 1 no significa
"asigna 1 a a", sino "haz que el patrón a coincida con 1".
# Esto funciona: a se vincula a 1
a = 1
# Esto también funciona: 1 coincide con el valor de a
1 = a
# Esto falla: 2 no coincide con 1
2 = a
# ** (MatchError) no match of right hand side value: 1
Viniendo de Rust, piensa en pattern matching como el match
de Rust pero omnipresente: no solo en expresiones match, sino en
asignaciones, argumentos de funciones, y case/cond/receive.
Matching en estructuras de datos
Tuplas
# Descomponer tupla
{status, valor} = {:ok, 42}
status # :ok
valor # 42
# Matching selectivo con valores literales
{:ok, resultado} = {:ok, 42} # Funciona
{:ok, resultado} = {:error, "fallo"} # MatchError!
# Ignorar valores con _
{:ok, _} = {:ok, 42} # Solo verificamos que sea :ok
Listas
# Head y tail
[head | tail] = [1, 2, 3]
head # 1
tail # [2, 3]
# Múltiples elementos del head
[a, b | rest] = [1, 2, 3, 4]
a # 1
b # 2
rest # [3, 4]
# Lista vacía
[] = [] # Funciona
[head | _] = [] # MatchError! Lista vacía no tiene head
Maps
# Extraer valores específicos
%{nombre: n, edad: e} = %{nombre: "Ana", edad: 30, ciudad: "Valencia"}
n # "Ana"
e # 30
# El match de maps es parcial: no necesitas todas las keys
%{nombre: n} = %{nombre: "Ana", edad: 30} # Funciona
# Pero la key debe existir
%{email: e} = %{nombre: "Ana"} # MatchError!
# Match con valores literales
%{tipo: :tick} = %{tipo: :tick, precio: 100} # Funciona
%{tipo: :quote} = %{tipo: :tick, precio: 100} # MatchError!
Para mensajes entre procesos, prefiere tuplas {:tipo, dato}
sobre maps %{tipo: :x, dato: y}. Las tuplas son más
eficientes para pattern matching y es la convención en Elixir/Erlang.
El operador pin ^
Por defecto, una variable en un pattern se rebind al nuevo
valor. El operador ^ fuerza a usar el valor existente:
x = 1
# Sin pin: x se rebindea a 2
{x, y} = {2, 3}
x # 2
# Resetear
x = 1
# Con pin: debe coincidir con el valor actual de x
{^x, y} = {1, 3} # Funciona, x sigue siendo 1
{^x, y} = {2, 3} # MatchError! 2 != 1
El pin es crucial en receive cuando necesitas filtrar
por un valor específico:
expected_ref = make_ref()
receive do
{^expected_ref, resultado} -> # Solo matchea si ref coincide
resultado
end
Pattern matching en funciones
Elixir permite definir múltiples cláusulas de una función con
diferentes patterns. Es como el match de Rust pero
integrado en la definición de funciones:
defmodule MessageHandler do
# Cláusula para tick de precio
def handle({:tick, symbol, precio}) do
IO.puts("Tick: #{symbol} @ $#{precio}")
end
# Cláusula para quote (bid/ask)
def handle({:quote, symbol, bid, ask}) do
spread = ask - bid
IO.puts("Quote: #{symbol} bid:$#{bid} ask:$#{ask} spread:#{spread}")
end
# Cláusula para errores
def handle({:error, razon}) do
IO.puts("Error: #{razon}")
end
# Catch-all para mensajes desconocidos
def handle(otro) do
IO.puts("Mensaje desconocido: #{inspect(otro)}")
end
end
MessageHandler.handle({:tick, "BTCUSD", 67543.21})
MessageHandler.handle({:quote, "EURUSD", 1.0842, 1.0845})
MessageHandler.handle(:desconocido)
Las cláusulas se evalúan en orden. Pon las más específicas primero y el catch-all al final. El compilador te advertirá si una cláusula nunca puede ejecutarse.
Guards
Los guards añaden condiciones adicionales al pattern matching usando
when:
defmodule PriceValidator do
# Solo acepta precios positivos
def validate({:precio, valor}) when is_number(valor) and valor > 0 do
{:ok, valor}
end
# Precio cero o negativo
def validate({:precio, valor}) when is_number(valor) do
{:error, :precio_invalido}
end
# No es un número
def validate({:precio, _}) do
{:error, :tipo_invalido}
end
end
Funciones permitidas en guards
Solo un subconjunto de funciones pueden usarse en guards. Las más comunes:
| Categoría | Funciones |
|---|---|
| Comparación | ==, !=, ===, !==, <, >, <=, >= |
| Booleanos | and, or, not, ! |
| Aritmética | +, -, *, /, div, rem |
| Tipo | is_atom, is_binary, is_float, is_integer, is_list, is_map, is_number, is_tuple |
| Tamaño | length, map_size, tuple_size, byte_size |
| Otros | hd, tl, elem, is_nil |
Binary pattern matching
Elixir tiene un sistema de pattern matching para binarios extremadamente potente, heredado de Erlang. Esto es crucial para protocolos financieros binarios:
# Parsear un mensaje de precio en formato binario
# Formato: tipo(1 byte) + symbol(8 bytes) + precio(8 bytes float)
mensaje = <<1, "BTCUSD", 0, 0, 67543.21::float>>
case mensaje do
<<1, symbol::binary-size(8), precio::float>> ->
symbol_clean = symbol |> String.trim_trailing(<<0>>)
{:tick, symbol_clean, precio}
<<2, symbol::binary-size(8), bid::float, ask::float>> ->
{:quote, symbol, bid, ask}
_ ->
{:error, :formato_desconocido}
end
Especificadores de tipo binario
| Especificador | Descripción |
|---|---|
integer |
Entero (por defecto) |
float |
Flotante de 64 bits |
binary |
Secuencia de bytes |
bits |
Secuencia de bits |
utf8 |
Codepoint UTF-8 |
size(n) |
Tamaño en bits/bytes |
big/little/native |
Endianness |
signed/unsigned |
Para enteros |
# Parsear entero de 32 bits little-endian
<<valor::little-integer-size(32)>> = <<0xD2, 0x04, 0x00, 0x00>>
valor # 1234
# Extraer los primeros 4 bytes y el resto
<<header::binary-size(4), rest::binary>> = "HEADdatos..."
header # "HEAD"
rest # "datos..."
Esta sintaxis viene directamente de Erlang y es una de las razones por las que Erlang se usa tanto en telecomunicaciones y protocolos de red. La sintaxis es idéntica en ambos lenguajes.
Caso de uso: Parser de protocolo FIX simplificado
FIX (Financial Information eXchange) es un protocolo común en finanzas. Aquí un parser simplificado:
defmodule SimpleFIX do
# FIX usa formato "tag=value|" donde | es SOH (0x01)
def parse(mensaje) do
mensaje
|> String.split(<<0x01>>) # Split por SOH
|> Enum.reject(&(&1 == ""))
|> Enum.map(&parse_field/1)
|> Map.new()
end
defp parse_field(field) do
case String.split(field, "=", parts: 2) do
[tag, value] -> {String.to_integer(tag), value}
_ -> {:error, field}
end
end
# Extraer campos comunes de un mensaje FIX
def extract_market_data(parsed) do
with {:ok, symbol} <- Map.fetch(parsed, 55), # Tag 55 = Symbol
{:ok, precio} <- Map.fetch(parsed, 44) do # Tag 44 = Price
{:ok, %{symbol: symbol, precio: String.to_float(precio)}}
else
:error -> {:error, :campos_faltantes}
end
end
end
# Ejemplo
fix_msg = "8=FIX.4.4" <> <<1>> <> "35=W" <> <<1>> <> "55=BTCUSD" <> <<1>> <> "44=67543.21" <> <<1>>
parsed = SimpleFIX.parse(fix_msg)
# %{8 => "FIX.4.4", 35 => "W", 55 => "BTCUSD", 44 => "67543.21"}
SimpleFIX.extract_market_data(parsed)
# {:ok, %{symbol: "BTCUSD", precio: 67543.21}}
with: encadenando matches
with permite encadenar múltiples pattern matches con
un manejo de errores limpio:
def process_market_update(mensaje) do
with {:ok, parsed} <- parse_message(mensaje),
{:ok, validated} <- validate_fields(parsed),
{:ok, enriched} <- enrich_data(validated) do
{:ok, enriched}
else
{:error, :parse_failed} -> {:error, "Formato inválido"}
{:error, :validation_failed} -> {:error, "Datos inválidos"}
{:error, :enrichment_failed} -> {:error, "Error de enriquecimiento"}
_ -> {:error, "Error desconocido"}
end
end
Escribe funciones que extraigan datos de estas estructuras usando pattern matching:
# 1. Extraer bid y ask de:
quote = %{symbol: "EURUSD", bid: 1.0842, ask: 1.0845, ts: 1234567890}
# 2. Extraer el primer y último precio de:
precios = [100.5, 101.2, 99.8, 102.1]
# 3. Extraer el símbolo si el status es :ok:
resultado = {:ok, {"BTCUSD", 67543.21}}
Crea un módulo MessageRouter con una función
route/1 que use pattern matching en funciones para
manejar diferentes tipos de mensajes:
{:tick, symbol, price}→ imprime "TICK: ..."{:quote, symbol, bid, ask}→ imprime "QUOTE: ..." con spread{:trade, symbol, price, volume}→ imprime "TRADE: ..."{:heartbeat, timestamp}→ imprime "HB: ..."- Cualquier otra cosa → imprime "UNKNOWN: ..."
Añade guards para validar que los precios sean positivos.
Implementa un parser para este formato binario de mensaje de precio:
# Formato del mensaje:
# - Header (4 bytes): "TICK" o "QUOT"
# - Symbol length (1 byte)
# - Symbol (variable, según length)
# - Precio (8 bytes, float big-endian)
# - Para QUOT: Bid (8 bytes float) + Ask (8 bytes float)
La función debe retornar {:tick, symbol, precio} o
{:quote, symbol, bid, ask} según el header.
Conexión con el proyecto final
Pattern matching será el corazón de nuestro sistema:
- Parsing de protocolos: binary pattern matching para parsear mensajes de feeds de datos.
- Routing de mensajes: función con múltiples cláusulas para dirigir datos a los subscribers correctos.
- Filtrado eficiente: guards para filtrar por símbolo, rango de precio, etc.