Cheatsheet Elixir
Linguagem funcional na BEAM VM para aplicações concorrentes e escaláveis
Elixir
Recursos Avançados
Protocols
# Definir protocolo:
defprotocol Descriptible do
def describir(dato)
end
# Implementar para tipos:
defimpl Descriptible, for: BitString do
def describir(s), do: "String: #{s}"
end
defimpl Descriptible, for: Integer do
def describir(n), do: "Número: #{n}"
end
Descriptible.describir("hola") # "String: hola"
Descriptible.describir(42) # "Número: 42"Un protocol define polimorfismo ad-hoc: declara una función con defprotocol y se implementa por tipo con defimpl. La función correcta se elige en runtime por el tipo del dato.
Behaviours
# Definir behaviour (contrato):
defmodule Worker do
@callback procesar(dato :: any) :: {:ok, any}
@callback nombre() :: String.t()
end
# Implementar:
defmodule MiWorker do
@behaviour Worker
@impl true
def procesar(d), do: {:ok, d}
@impl true
def nombre, do: "MiWorker"
endUn behaviour define un contrato de callbacks con @callback. El módulo que lo adopta usa @behaviour y marca cada implementación con @impl true, garantizado en compilación.
defimpl y Any
# Para structs:
defimpl Descriptible, for: User do
def describir(u), do: "User: #{u.nombre}"
end
# Varios tipos a la vez:
defimpl Descriptible, for: [List, Tuple] do
def describir(d), do: "Colección: #{inspect(d)}"
end
# Fallback para cualquier tipo:
defimpl Descriptible, for: Any do
def describir(d), do: inspect(d)
endEl defimpl implementa el protocolo para un tipo (o varios, con una lista). El for: Any define un fallback para tipos sin implementación específica, evitando errores.
use y __using__
# use inyecta código de un módulo:
defmodule MiHelper do
defmacro __using__(_opts) do
quote do
import MiHelper
@before_compile MiHelper
end
end
end
# Uso:
defmodule App do
use MiHelper
# ahora tiene el código inyectado
endEl use llama al macro __using__ de un módulo, que inyecta código (imports, callbacks, etc.) en el módulo actual. Así funcionan GenServer y Supervisor por debajo.
Macros
defmodule Logger do
defmacro log(expr) do
quote do
IO.puts("[LOG] #{inspect(unquote(expr))}")
unquote(expr)
end
end
end
require Logger
Logger.log(2 + 2)
# [LOG] 4
# => 4Un macro (con defmacro) recibe código y devuelve código, ejecutado en tiempo de compilación. El quote crea el AST y el unquote inserta valores. Requiere require para usarse.
Atributos de módulo
defmodule Config do
# Atributos de módulo (@nombre):
@version "1.0.0"
@timeout 5000
def version, do: @version
# Acumulador (registrar varios):
Module.register_attribute(__MODULE__,
:rutas, accumulate: true)
@ruta "/home"
@ruta "/acerca"
def rutas, do: @rutas # ["/acerca", "/home"]
endLos atributos de módulo (@nombre) son constantes evaluadas en compilación. Sirven para configuración, documentación y, con accumulate: true, para registrar listas de valores.
quote y unquote
# quote: transforma código en AST
quote do
1 + 2
end
# {:+, [], [1, 2]}
# unquote: inserta valor en el AST
x = 5
quote do
unquote(x) + 10
end
# {:+, [], [5, 10]}
# La macro hygiene evita conflictos de nombresEl quote convierte código en su representación AST (árbol de sintaxis). El unquote inyecta un valor dentro de un quote. Son la base de la metaprogramación en Elixir.
Sigils custom y docs
# Documentación con @doc y @moduledoc:
defmodule Math do
@moduledoc "Funciones matemáticas"
@doc """
Duplica un número.
## Ejemplos
iex> Math.doble(2)
4
"""
def doble(x), do: x * 2
end
# Doctests:
# ExUnit valida los ejemplos iex>El @moduledoc documenta el módulo y el @doc documenta funciones. Los ejemplos con iex> se convierten en doctests validados automáticamente por ExUnit.
Básico
Variables e inmutabilidad
nombre = "Elixir" edad = 30 activo = true pi = 3.14 # La reasignación crea un nuevo binding: x = 1 x = 2 # x ahora es 2 (no muta el 1) # Todo es inmutable: # las operaciones devuelven nuevos valores
Las variables se asignan con = y todos los datos son inmutables. Reasignar una variable crea un nuevo binding; los valores originales nunca se modifican.
Pipe operator
# Sin pipe (anidado):
String.upcase(String.trim(" hola "))
# Con pipe (|>):
" hola "
|> String.trim()
|> String.upcase()
|> String.reverse()
# El resultado de cada paso es el
# primer argumento del siguienteEl operador |> (pipe) pasa el resultado de la expresión anterior como primer argumento de la siguiente. Hace legibles los pipelines de transformaciones, evitando llamadas anidadas.
Sigils
# ~w: lista de palabras
~w(palabra1 palabra2) # ["palabra1", "palabra2"]
~w(a b c)a # [:a, :b, :c] (atoms)
# ~r: expresión regular
~r/\d+/ # regex de dígitos
Regex.run(~r/(\d+)/, "abc123") # ["123", "123"]
# ~s: string con interpolación
~s(hola #{nombre})
# ~D, ~T, ~N: fechas y tiempo
~D[2024-01-15]Los sigils empiezan con ~ y crean valores con sintaxis especial: ~w (listas de palabras), ~r (regex), ~s (strings) y ~D (fechas). Modificadores como a cambian el tipo.
Tipos básicos
# Tipos primitivos:
42 # integer
3.14 # float
"texto" # string (binario)
?a # char (97)
true # boolean
:atomo # atom
# Estructuras:
{1, 2, 3} # tuple
[1, 2, 3] # list
%{a: 1} # map
<<1, 2>> # bitstringElixir tiene integers, floats, strings (binarios), atoms (:nombre), booleans y estructuras como tuples, lists y maps.
IO (entrada/salida)
# Imprimir con newline:
IO.puts("Hola Mundo")
# Inspección (debug, muestra estructura):
IO.inspect(%{a: 1, b: 2})
IO.inspect(lista, label: "Datos")
# Leer input:
nombre = IO.gets("Nombre? ")
# Inspect devuelve string (no imprime):
inspect([1, 2, 3]) # "[1, 2, 3]"IO.puts imprime texto con salto de línea e IO.inspect muestra la representación de cualquier valor (excelente para debug). IO.gets lee una línea del stdin.
Operadores
# Aritméticos: 2 + 3 # 5 10 - 4 # 6 3 * 7 # 21 10 / 2 # 5.0 (siempre float) div(10, 3) # 3 (entero) rem(10, 3) # 1 (resto) # Comparación: 1 == 1 # true 1 != 2 # true 1 === 1.0 # false (estricto: int vs float) # Lógicos (solo booleans): true and false # false true or false # true not true # false
Los operadores incluyen + - * / (la división / devuelve siempre float; usa div/rem para enteros). La comparación estricta === distingue 1 de 1.0.
Atoms
# Atom: constante cuyo nombre es el valor
:ok
:error
:nombre
# Los booleans son atoms:
true == :true # true
false == :false # true
nil == :nil # true
# Atoms con espacios:
:"hola mundo"
# Uso común en tuplas de resultado:
{:ok, valor}
{:error, razon}Un atom es una constante cuyo nombre es el propio valor (empieza con :). Los booleanos true/false y nil son atoms. Se usan mucho en tuplas {:ok, _} y {:error, _}.
Strings
s = "Hola Mundo"
String.upcase(s) # "HOLA MUNDO"
String.downcase(s) # "hola mundo"
String.length(s) # 10
String.trim(" eh ") # "eh"
String.split(s, " ") # ["Hola", "Mundo"]
String.replace(s, "Mundo", "Elixir")
String.contains?(s, "Mundo") # true
# Las strings son binarios UTF-8Las strings son binarios UTF-8. El módulo String ofrece operaciones como upcase, split, replace y contains?, todas inmutables (devuelven nuevas strings).
Módulos
defmodule Saludo do
def hola(nombre) do
"¡Hola, #{nombre}!"
end
# Función privada:
defp secreto do
"interno"
end
end
# Llamar:
Saludo.hola("Mundo") # "¡Hola, Mundo!"El código se organiza en módulos con defmodule. Las funciones públicas se definen con def y las privadas con defp (solo visibles dentro del módulo).
Interpolación y concatenación
nombre = "Ana"
edad = 30
# Interpolación con #{}:
"¡Hola, #{nombre}!" # "¡Hola, Ana!"
"2 + 2 = #{2 + 2}" # "2 + 2 = 4"
# Concatenación:
"Hola" <> " " <> "Mundo" # "Hola Mundo"
# Multilínea (heredoc):
texto = """
Línea 1
Línea 2
"""La interpolación #{expr} inserta valores en una string. El operador <> concatena binarios. Las strings de varias líneas usan tres comillas (heredoc).
Funções
def y defp
defmodule Math do
# Cuerpo largo:
def doble(x) do
x * 2
end
# Cuerpo corto (one-liner):
def triple(x), do: x * 3
# Privada (solo en el módulo):
defp auxiliar(x), do: x + 1
end
Math.doble(5) # 10Las funciones se definen con def (públicas) o defp (privadas). La forma do: se usa para cuerpos de una línea; el bloque do...end para varias.
Guards
def clasificar(edad) when edad < 12 do "Niño" end def clasificar(edad) when edad < 18 do "Adolescente" end def clasificar(_), do: "Adulto" # Funciones de guarda: def procesar(x) when is_integer(x), do: "int" def procesar(x) when is_list(x), do: "lista" # is_atom, is_binary, is_number, >, <, ==
Los guards (when) añaden condiciones a las cláusulas. Usan funciones permitidas como is_integer, is_list, is_atom y operadores de comparación.
Default args y cláusulas
# Argumento con default (\\):
def saludar(nombre, punt \\ "!") do
"Hola, #{nombre}#{punt}"
end
saludar("Ana") # "Hola, Ana!"
saludar("Ana", "?") # "Hola, Ana?"
# Varias cláusulas de la misma función:
def fact(0), do: 1
def fact(n), do: n * fact(n - 1)Los argumentos opcionales usan \ para definir un valor por defecto. Una función puede tener varias cláusulas (mismo nombre/aridad), probadas en orden hasta casar.
Pattern matching en parámetros
# Casa por estructura:
def area({:círculo, r}), do: :math.pi * r * r
def area({:rect, l, a}), do: l * a
area({:círculo, 2}) # 12.56...
area({:rect, 3, 4}) # 12
# Múltiples cláusulas por pattern:
def describir([]), do: "vacía"
def describir([_ | _]), do: "con elementos"Los parámetros hacen pattern matching: cada cláusula casa con una forma específica. Elixir prueba las cláusulas en orden hasta que una casa, permitiendo funciones polimórficas elegantes.
Funciones anónimas (fn)
# Definir: doble = fn x -> x * 2 end suma = fn a, b -> a + b end # Llamar con punto: doble.(5) # 10 suma.(3, 4) # 7 # Varias cláusulas: tipo = fn x when is_integer(x) -> "int" x when is_binary(x) -> "string" end tipo.(42) # "int"
Las funciones anónimas se crean con fn ... end y se llaman con un punto (doble.(5)). Pueden tener varias cláusulas con pattern matching y guards.
Destructuring
# Tuplas:
{x, y, z} = {1, 2, 3} # x=1, y=2, z=3
# Listas (head/tail):
[head | tail] = [1, 2, 3, 4] # head=1, tail=[2,3,4]
# Maps:
%{nombre: n} = %{nombre: "Ana", edad: 30} # n="Ana"
# Ignorar con _:
{_, segundo, _} = {10, 20, 30} # segundo=20El destructuring extrae partes de estructuras en el lado izquierdo del =. El _ ignora posiciones y el [head | tail] separa el primer elemento del resto de la lista.
Shorthand &()
# &() es atajo para fn:
doble = &(&1 * 2) # fn x -> x * 2 end
suma = &(&1 + &2) # fn a, b -> a + b end
# &1, &2... son los parámetros:
Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6]
# Mezclar con texto:
saludar = &"¡Hola, #{&1}!"
saludar.("Ana") # "¡Hola, Ana!"La sintaxis &() es una forma corta de función anónima, donde &1, &2 representan los parámetros por posición. Es ideal para funciones pequeñas en Enum.
Pin operator (^)
x = 1
# Sin pin: re-bind (x pasa a 2)
x = 2
# Con pin: casa con el valor actual
^x = 2 # OK, x es 2
^x = 3 # ¡MatchError! (x es 2, no 3)
# Útil en parámetros y case:
case {1, 2} do
{^x, y} -> "x casó, y=#{y}"
endEl operador ^ (pin) impide el re-binding y fuerza la comparación con el valor actual de la variable. Sin él, el = reasigna; con él, hace match contra el valor existente.
Captura de función
# Capturar función nombrada:
upcase = &String.upcase/1
upcase.("hola") # "HOLA"
# Notación Módulo.función/aridad:
Enum.map(["a", "b"], &String.upcase/1)
# ["A", "B"]
# Captura parcial:
doble = &Kernel.*/2
# más común: &(&1 * 2)La captura &Módulo.función/aridad transforma una función nombrada en anónima (ej.: &String.upcase/1). La aridad es el número de argumentos.
Estruturas de Dados
Lists
lista = [1, 2, 3, 4] # Constructor cons (|): [0 | lista] # [0, 1, 2, 3, 4] # Acceso: hd(lista) # 1 (head) tl(lista) # [2, 3, 4] (tail) length(lista) # 4 # Las listas son enlazadas: # añadir al inicio es O(1)
Las lists son listas enlazadas: añadir al inicio con [x | lista] es eficiente (O(1)). El hd da la cabeza, el tl el resto y length el tamaño.
Tuples
# Tuple: tamaño fijo, acceso por índice
point = {3, 4}
elem(point, 0) # 3
elem(point, 1) # 4
tuple_size(point) # 2
# Actualizar posición:
put_elem(point, 1, 5) # {3, 5}
# Uso común: retornos
{:ok, datos}
{:error, "falló"}Las tuples tienen tamaño fijo y acceso por índice con elem. Son ideales para retornos como {:ok, valor}. A diferencia de las lists, no son iterables.
Binaries y bitstrings
# Binary (secuencia de bytes):
<<1, 2, 3>> # 3 bytes
"hola" # es un binary
# Tamaño en bits:
<<x::8, y::8>> = <<10, 20>> # x=10, y=20
# Concatenar:
<<1, 2>> <> <<3>> # <<1, 2, 3>>
# Las strings son binaries:
is_binary("hola") # true
byte_size("hola") # 4Un binary (<< >>) es una secuencia de bytes; una bitstring puede tener tamaño en bits. Las strings son binaries UTF-8. El <<x::8>> especifica el tamaño.
Operaciones con lists
a = [1, 2, 3] # Concatenación: a ++ [4, 5] # [1, 2, 3, 4, 5] # Diferencia: [1, 2, 3] -- [2] # [1, 3] # Pertenencia: 3 in a # true 9 in a # false # Reversión: Enum.reverse(a) # [3, 2, 1]
El ++ concatena dos listas y el -- elimina elementos. El operador in comprueba pertenencia. Estas operaciones recorren la lista, siendo O(n).
Keyword lists
# Lista de pares {atom, valor}:
opts = [nombre: "App", debug: true]
# Equivale a:
[{:nombre, "App"}, {:debug, true}]
# Acceso:
opts[:nombre] # "App"
Keyword.get(opts, :debug) # true
Keyword.get(opts, :puerto, 4000) # default
# Muy usada en opciones de funcionesUna keyword list es una lista de tuplas {atom, valor}, escritura como [clave: valor]. Mantiene el orden y permite claves repetidas; es común en opciones de funciones.
Maps
persona = %{nombre: "Ana", edad: 30}
# Acceso:
persona.nombre # "Ana" (solo atom keys)
persona[:edad] # 30
Map.get(persona, :email, "N/A") # "N/A"
# Claves:
Map.keys(persona) # [:nombre, :edad]
Map.values(persona) # ["Ana", 30]
Map.has_key?(persona, :nombre) # trueLos maps (%{}) guardan pares clave-valor sin orden. Con claves atom, se accede por punto (persona.nombre). El Map.get acepta un valor default.
Structs
defmodule User do
defstruct nombre: "", edad: 0, activo: true
end
# Crear:
u = %User{nombre: "Ana", edad: 30}
# Acceso:
u.nombre # "Ana"
# Actualizar:
%{u | edad: 31}
# Un struct es un map con __struct__:
%User{} = u # pattern match por tipoUn struct es un map con claves fijas y valores por defecto, definido con defstruct. Garantiza la forma de los datos y permite pattern matching por tipo (%User{}).
Actualizar maps
persona = %{nombre: "Ana", edad: 30}
# Sintaxis de update (solo claves existentes):
%{persona | edad: 31}
# Map.put (añade o actualiza):
Map.put(persona, :email, "ana@mail.com")
# Map.update (con función):
Map.update(persona, :edad, 0, &(&1 + 1))
# Map.delete:
Map.delete(persona, :edad)Los maps son inmutables. La sintaxis %{m | clave: valor} actualiza claves existentes; Map.put añade o sustituye, Map.update aplica una función y Map.delete elimina.
Acceso anidado
datos = %{user: %{nombre: "Ana", direccion: %{ciudad: "Oporto"}}}
# get_in: lee un camino
get_in(datos, [:user, :direccion, :ciudad])
# "Oporto"
# update_in: actualiza un camino
update_in(datos.user.edad, &(&1 + 1))
# put_in: define un camino
put_in(datos[:user][:email], "ana@mail.com")
# pop_in: elimina y devuelvePara estructuras anidadas, get_in lee un camino de claves, update_in aplica una función en ese camino y put_in define un valor — todo de forma inmutable.
Enum e Stream
map, filter, reduce
# map: transforma cada elemento Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6] # filter: mantiene los que pasan Enum.filter([1, 2, 3, 4], &(&1 > 2)) # [3, 4] # reduce: agrega con acumulador Enum.reduce([1, 2, 3], 0, &(&1 + &2)) # 6
El Enum.map transforma cada elemento, el Enum.filter selecciona los que pasan el predicado y el Enum.reduce combina todo en un valor con un acumulador inicial.
Agregación
Enum.sum([1, 2, 3, 4]) # 10 Enum.max([3, 1, 4]) # 4 Enum.min([3, 1, 4]) # 1 Enum.count([1, 2, 3]) # 3 # Con predicado: Enum.count([1, 2, 3, 4], &(&1 > 2)) # 2 # Por función: Enum.max_by(users, & &1.edad) Enum.min_by(users, & &1.edad)
Estas funciones resumen colecciones: sum, max, min y count. El count acepta un predicado y max_by/min_by usan una función de criterio.
Ranges y comprehensions
# Range:
1..10
Enum.to_list(1..5) # [1, 2, 3, 4, 5]
Enum.sum(1..100) # 5050
# Comprehension (for):
for x <- 1..5, do: x * x
# [1, 4, 9, 16, 25]
# Con filtro:
for x <- 1..10, even?(x), do: x
# Múltiples generadores:
for x <- 1..3, y <- 1..3, do: {x, y}Un range (1..10) es una secuencia lazy de enteros. La comprehension for construye listas con generadores (<-) y filtros, similar a las list comprehensions.
each y find
# each: efecto secundario (no transforma) Enum.each([1, 2, 3], &IO.puts/1) # find: primero que pasa Enum.find([1, 2, 3, 4], &(&1 > 2)) # 3 # find con default: Enum.find([1, 2], &(&1 > 10), :ninguno) # :ninguno # find_index: posición Enum.find_index([10, 20, 30], &(&1 == 20)) # 1
El Enum.each ejecuta un efecto secundario sin devolver lista. El Enum.find devuelve el primer elemento que satisface el predicado (o un default) y el find_index su posición.
group_by y frequencies
# group_by: agrupa por función
Enum.group_by(1..10, &rem(&1, 2))
# %{0 => [2,4,6,8,10], 1 => [1,3,5,7,9]}
Enum.group_by(users, & &1.ciudad)
# frequencies: cuenta ocurrencias
Enum.frequencies(~w(a b a c a b))
# %{a: 3, b: 2, c: 1}
# frequencies_by:
Enum.frequencies_by(users, & &1.tipo)El Enum.group_by agrupa elementos en un map por una función clave. El Enum.frequencies cuenta cuántas veces aparece cada valor; frequencies_by cuenta por un criterio.
sort y uniq
Enum.sort([3, 1, 2]) # [1, 2, 3] Enum.sort([3, 1, 2], :desc) # [3, 2, 1] # sort_by: ordena por función Enum.sort_by(users, & &1.edad) Enum.sort_by(users, & &1.edad, :desc) # uniq: elimina duplicados Enum.uniq([1, 1, 2, 3, 3]) # [1, 2, 3] Enum.uniq_by(users, & &1.email)
El Enum.sort ordena (con :desc para descendente) y el sort_by ordena por una función. El Enum.uniq elimina duplicados; uniq_by por un criterio.
any?, all?, member?
# any?: ¿alguno pasa? Enum.any?([1, 2, 3], &(&1 > 2)) # true # all?: ¿todos pasan? Enum.all?([1, 2, 3], &(&1 > 0)) # true # member?: ¿contiene el valor? Enum.member?([1, 2, 3], 2) # true # empty?: Enum.empty?([]) # true # take / drop: Enum.take([1, 2, 3, 4], 2) # [1, 2] Enum.drop([1, 2, 3, 4], 2) # [3, 4]
El Enum.any? verifica si algún elemento pasa, el all? si todos pasan y el member? si contiene un valor. El take/drop retiran N elementos.
chunk, zip, flat_map
# chunk_every: agrupa en bloques
Enum.chunk_every([1, 2, 3, 4, 5], 2)
# [[1, 2], [3, 4], [5]]
# zip: combina en pares
Enum.zip([1, 2], [:a, :b]) # [{1, :a}, {2, :b}]
# flat_map: map + flatten
Enum.flat_map([[1, 2], [3, 4]], & &1)
# [1, 2, 3, 4]
# with_index:
Enum.with_index([:a, :b]) # [{:a, 0}, {:b, 1}]El chunk_every agrupa en bloques de N, el zip combina dos colecciones en pares y el flat_map mapea y aplana. El with_index junta cada elemento con su índice.
Streams (lazy)
# Stream: evaluación perezosa
1..1_000_000
|> Stream.filter(&(rem(&1, 2) == 0))
|> Stream.map(&(&1 * &1))
|> Enum.take(5)
# [4, 16, 36, 64, 100]
# Stream de fichero (línea a línea):
File.stream!("grande.txt")
|> Stream.map(&String.trim/1)
|> Stream.filter(&(&1 != ""))
|> Enum.to_list()El Stream es lazy: solo procesa los elementos cuando es necesario (con Enum.take o to_list). Es ideal para datos grandes o infinitos, evitando crear listas intermedias.
Processos
spawn
# Crear proceso:
pid = spawn(fn ->
IO.puts("Hola desde otro proceso")
end)
# pid es el identificador (#PID<0.123.0>)
Process.alive?(pid) # true/false
# spawn con módulo/función:
spawn(MiModulo, :funcion, [arg1, arg2])
# Los procesos son ligeros (miles OK)El spawn crea un proceso concurrente ligero y devuelve su pid. Los procesos de la BEAM están aislados y son baratos — es común tener miles en simultáneo.
Task async/await
# Task: computación asíncrona task = Task.async(fn -> Process.sleep(1000) 42 end) # Hacer otro trabajo... # await bloquea hasta el resultado: resultado = Task.await(task, 5000) # 42 # Varias tasks en paralelo: tasks = Enum.map(1..5, &Task.async(fn -> &1 * 2 end)) Enum.map(tasks, &Task.await/1)
Una Task ejecuta trabajo asíncrono. El Task.async inicia y devuelve la task; el Task.await bloquea hasta obtener el resultado. Varias tasks corren en paralelo.
Callbacks del GenServer
defmodule Contador do
use GenServer
# Inicialización:
def init(v), do: {:ok, v}
# Trata call (síncrono):
def handle_call(:get, _from, n) do
{:reply, n, n}
end
# Trata cast (asíncrono):
def handle_cast(:inc, n) do
{:noreply, n + 1}
end
endLos callbacks del GenServer tratan los mensajes: init define el estado inicial, handle_call responde a call ({:reply, resp, estado}) y handle_cast trata cast ({:noreply, estado}).
send y receive
# Enviar mensaje:
send(pid, {:hola, "Mundo"})
# Recibir (bloquea hasta que llegue):
receive do
{:hola, msg} -> IO.puts(msg)
{:adios, _} -> IO.puts("Adiós")
_ -> IO.puts("Desconocido")
end
# self() es el pid del proceso actual
send(self(), :ping)Los procesos se comunican por mensajes: send envía a un pid y receive bloquea hasta que llegue un mensaje que case. El self() es el pid del proceso actual.
Task.async_stream
# Procesar colección en paralelo:
1..10
|> Task.async_stream(&procesar/1,
max_concurrency: 4)
|> Enum.map(fn {:ok, r} -> r end)
# timeout y on_timeout:
|> Task.async_stream(&lento/1,
timeout: 3000,
on_timeout: :kill_task)El Task.async_stream procesa una colección en paralelo, preservando el orden. El max_concurrency limita procesos simultáneos; el timeout controla el tiempo por tarea.
receive con timeout
receive do
{:resultado, valor} -> valor
after
5000 -> IO.puts("Timeout (5s)")
end
# after define el tiempo máximo de espera
# (en milisegundos)
# 0 = no bloquear (verifica ya)
receive do
msg -> msg
after
0 -> :nada
endLa cláusula after en el receive define un timeout en milisegundos; si no llega ningún mensaje, ejecuta la rama de timeout. Con 0, verifica sin bloquear.
Agent
# Agent: estado simple compartido
{:ok, pid} = Agent.start_link(fn -> 0 end)
# Leer:
Agent.get(pid, & &1) # 0
# Actualizar:
Agent.update(pid, &(&1 + 1)) # ok
Agent.get(pid, & &1) # 1
# Leer y actualizar:
Agent.get_and_update(pid, fn s ->
{s, s + 10}
end)
# Con nombre:
Agent.start_link(fn -> %{} end, name: MiEstado)Un Agent guarda estado compartido de forma simple. El Agent.get lee, el Agent.update modifica y el get_and_update hace ambos. Ideal para estado simple.
Process (info y diccionario)
# Información del proceso: Process.info(self()) Process.alive?(pid) # Diccionario del proceso (estado local): Process.put(:clave, "valor") Process.get(:clave) # "valor" # Links: Process.link(pid) Process.monitor(pid)
El módulo Process da información (info, alive?) y un diccionario local (put/get). El link enlaza procesos; el monitor observa sin propagar fallos.
GenServer (call y cast)
defmodule Contador do
use GenServer
def start_link(init \\ 0) do
GenServer.start_link(__MODULE__, init,
name: __MODULE__)
end
# API síncrona (responde):
def value, do: GenServer.call(__MODULE__, :get)
# API asíncrona (no responde):
def increment, do: GenServer.cast(__MODULE__, :inc)
endEl GenServer es un proceso con estado. El GenServer.call es síncrono (espera respuesta) y el GenServer.cast es asíncrono (no espera). La API pública encapsula los mensajes.
Controlo e Erros
case
case {1, 2, 3} do
{x, y, z} when x > 0 -> "positivo"
{0, _, _} -> "cero"
_ -> "otro"
end
# Con resultado de función:
case File.read("f.txt") do
{:ok, contenido} -> contenido
{:error, razon} -> "Error: #{razon}"
endEl case hace pattern matching de un valor contra varios patrones, con guards opcionales. El _ es el caso por defecto. Es ideal para tratar tuplas {:ok, _}/{:error, _}.
try y rescue
try do
raise "¡Algo falló!"
rescue
e in RuntimeError -> IO.puts(e.message)
e -> IO.puts("Otro: #{inspect(e)}")
after
IO.puts("Siempre se ejecuta")
end
# rescue por tipo de excepción:
# RuntimeError, ArgumentError,
# ArithmeticError, KeyErrorEl try/rescue captura excepciones por tipo (ej.: RuntimeError). El bloque after se ejecuta siempre. En Elixir se prefieren tuplas {:error, _} a excepciones.
cond
# cond: múltiples condiciones (if/elseif) cond do x > 10 -> "grande" x > 5 -> "medio" true -> "pequeño" # default end # Equivalente a ifs anidados: # Cada rama es una expresión booleana # El true final es el else
El cond evalúa varias condiciones en secuencia, como un if/elseif. Cada rama es una expresión booleana; el true final funciona como caso por defecto (else).
raise y excepciones
# Lanzar excepción: raise "Error simple" raise ArgumentError, "valor inválido" # Definir excepción custom: defmodule MiError do defexception message: "error por defecto" end raise MiError, message: "fallo custom" # try/rescue las captura
El raise lanza una excepción con un mensaje o tipo. Las excepciones custom se definen con defexception. En Elixir, las excepciones son para situaciones verdaderamente excepcionales.
if y unless
# if (forma corta): if x > 0, do: "pos", else: "neg" # if (bloque): if x > 0 do "positivo" else "negativo" end # unless (negativo): unless x > 0 do "no positivo" end # En Elixir, if es una macro/expresión
El if devuelve un valor y tiene forma corta (do:/else:) o en bloque. El unless es el if negado. Ambos son expresiones que devuelven el resultado de la rama ejecutada.
Recursión
defmodule Math do
# Caso base + caso recursivo:
def factorial(0), do: 1
def factorial(n) when n > 0 do
n * factorial(n - 1)
end
# Sumar lista:
def sum([]), do: 0
def sum([h | t]), do: h + sum(t)
end
Math.factorial(5) # 120La recursión sustituye a los bucles en Elixir (no hay for/while imperativos). Se define un caso base y un caso recursivo, frecuentemente con pattern matching en listas.
with (happy path)
# Encadenar operaciones que pueden fallar:
with {:ok, user} <- find_user(id),
{:ok, posts} <- get_posts(user) do
{:ok, posts}
else
{:error, reason} -> {:error, reason}
:not_found -> {:error, "no existe"}
end
# Si algún <- falla, salta al elseEl with encadena operaciones en el "camino feliz": cada <- hace match; si todos casan, se ejecuta el bloque do. Si alguno falla, salta al else.
Recursión con acumulador
defmodule Math do
# Tail-recursive con acumulador:
def factorial(n), do: do_fact(n, 1)
defp do_fact(0, acc), do: acc
defp do_fact(n, acc) do
do_fact(n - 1, n * acc)
end
end
# El acumulador evita crecer la stack
# (tail-call optimization en la BEAM)La recursión con acumulador (tail-recursive) está optimizada en la BEAM: el resultado se pasa como argumento, evitando crecer la stack. Es la forma preferida para rendimiento.
OTP e Supervisão
Supervisor
defmodule MiApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts)
end
def init(_opts) do
children = [
Contador,
MiGenServer
]
Supervisor.init(children,
strategy: :one_for_one)
end
endUn Supervisor gestiona procesos hijos y los reinicia si fallan. Los hijos se listan en children y la estrategia define cómo reiniciar. Es la base de la tolerancia a fallos.
DynamicSupervisor
# Supervisor para hijos dinámicos:
children = [
{DynamicSupervisor,
name: MiApp.DynSup,
strategy: :one_for_one}
]
# Iniciar hijo en runtime:
DynamicSupervisor.start_child(
MiApp.DynSup,
{Worker, arg}
)
# Contar hijos:
DynamicSupervisor.count_children(MiApp.DynSup)El DynamicSupervisor supervisa hijos creados en tiempo de ejecución (no predefinidos). El start_child añade procesos dinámicamente — ideal para un número variable de workers.
Estrategias de supervisión
# :one_for_one - solo el que falló Supervisor.init(children, strategy: :one_for_one) # :one_for_all - todos reinician strategy: :one_for_all # :rest_for_one - el fallado y los siguientes strategy: :rest_for_one # max_restarts / max_seconds: Supervisor.init(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 5)
Las estrategias definen el reinicio: :one_for_one (solo el fallado), :one_for_all (todos) y :rest_for_one (el fallado y los siguientes). max_restarts limita reinicios.
ETS
# ETS: tabla en memoria (¡mutable!)
tabla = :ets.new(:cache, [:set, :public])
# Insertar:
:ets.insert(tabla, {:clave, "valor"})
:ets.insert(tabla, [{:a, 1}, {:b, 2}])
# Buscar:
:ets.lookup(tabla, :clave) # [{:clave, "valor"}]
# Borrar:
:ets.delete(tabla, :clave)
# Named table:
:ets.new(:cache, [:set, :named_table, :public])El ETS (Erlang Term Storage) es una tabla en memoria compartida y mutable, usada como caché de alto rendimiento. Tipos: :set, :ordered_set, :bag. Se accede vía el módulo :ets.
Application
# mix.exs define la aplicación:
def application do
[
mod: {MiApp.Application, []},
extra_applications: [:logger]
]
end
# Application inicia el árbol:
defmodule MiApp.Application do
use Application
def start(_type, _args) do
children = [Contador]
Supervisor.start_link(children,
strategy: :one_for_one)
end
endUna Application es el punto de entrada que arranca el árbol de supervisión. Se define en mix.exs con mod: e implementa el callback start/2.
Links y monitors
# Link: el fallo se propaga (bidireccional)
Process.link(pid)
# spawn_link: crea ya enlazado
spawn_link(fn -> tarea() end)
# Monitor: notifica sin propagar
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, razon} ->
IO.puts("Murió: #{inspect(razon)}")
endUn link une dos procesos: si uno falla, el otro también muere (propagación). Un monitor solo notifica el fallo (mensaje :DOWN) sin propagar, permitiendo reaccionar.
Registry
# Registry: nombrar procesos dinámicos
children = [
{Registry, keys: :unique, name: MiApp.Registry}
]
# Registrar proceso:
Registry.register(MiApp.Registry, "user_1", nil)
# Buscar pid:
Registry.lookup(MiApp.Registry, "user_1")
# [{pid, valor}]
# Enviar a todos con una clave:
Registry.dispatch(MiApp.Registry, "sala", fn entries ->
for {pid, _} <- entries, do: send(pid, :msg)
end)El Registry permite nombrar y buscar procesos dinámicos por clave. Con keys: :unique cada clave tiene un proceso; keys: :duplicate permite varios (útil para pub/sub).
Mix e Ferramentas
mix new
# Crear proyecto: mix new mi_app # Con árbol de supervisión: mix new mi_app --sup # Estructura generada: # mi_app/ # lib/mi_app.ex # test/mi_app_test.exs # mix.exs # README.md
El mix new crea un nuevo proyecto con la estructura estándar. El flag --sup añade un árbol de supervisión (Application). El mix.exs es el fichero de configuración.
mix format
# Formatear todo el proyecto:
mix format
# Verificar sin alterar (CI):
mix format --check-formatted
# Configuración en .formatter.exs:
[
inputs: ["{mix,.formatter}.exs",
"{config,lib,test}/**/*.{ex,exs}"],
line_length: 98
]El mix format formatea el código según el estilo oficial de Elixir. El --check-formatted valida sin alterar (útil en CI). La configuración queda en .formatter.exs.
mix.exs y dependencias
defmodule MiApp.MixProject do
use Mix.Project
def project do
[app: :mi_app, version: "0.1.0"]
end
def application do
[extra_applications: [:logger]]
end
defp deps do
[
{:phoenix, "~> 1.7"},
{:jason, "~> 1.4"}
]
end
endEl mix.exs configura el proyecto: project (nombre/versión), application (apps OTP) y deps (dependencias de Hex). La versión usa el formato ~> 1.7 (compatible).
IEx (REPL)
# Iniciar REPL: iex iex -S mix # con el proyecto # Ayuda: h Enum.map # docs i "hola" # info del término # Continuar expresión incompleta: # Enter en línea incompleta continúa # Recompilar en runtime: recompile() # h() lista ayuda, c() compila fichero
El IEx es el REPL interactivo. El iex -S mix carga el proyecto. El h muestra documentación, el i inspecciona un término y el recompile() recompila en runtime.
Comandos mix
# Dependencias: mix deps.get # descargar mix deps.update jason # actualizar # Compilar: mix compile # Ejecutar: mix run # script mix run -e "IO.puts(1)" # inline # Tareas comunes: mix format # formatear código mix test # tests mix help # listar tareas
El mix es la herramienta de build: deps.get descarga dependencias, compile compila, run ejecuta, format formatea y test corre los tests.
Estructura de proyecto
mi_app/
lib/
mi_app.ex # módulo principal
mi_app/
servidor.ex # MiApp.Servidor
test/
mi_app_test.exs
test_helper.exs
config/
config.exs
mix.exs
.formatter.exs
# Nombre del fichero -> módulo:
# lib/mi_app/servidor.ex -> MiApp.ServidorUn proyecto se organiza en lib/ (código), test/ (tests) y config/. La ruta del fichero define el módulo: lib/mi_app/servidor.ex es MiApp.Servidor.
mix test y ExUnit
defmodule MathTest do
use ExUnit.Case
test "doble de 2 es 4" do
assert Math.doble(2) == 4
end
test "lista no vacía" do
refute Enum.empty?([1, 2])
end
test "lanza error" do
assert_raise ArgumentError, fn ->
raise ArgumentError
end
end
endEl ExUnit es el framework de tests. Los tests usan test con assert/refute y corren con mix test. El assert_raise verifica excepciones.