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!
💡 Maps vs keyword lists para mensajes

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)
⚠️ Orden de cláusulas

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..."
🔴 Binary pattern matching es una feature de Erlang

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
Ejercicio 3.1 Descomponer datos financieros Básico

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}}
Ejercicio 3.2 Router de mensajes Intermedio

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.

Ejercicio 3.3 Parser binario Avanzado

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: