DevTools

Cheatsheet Elixir

Linguagem funcional na BEAM VM para aplicações concorrentes e escaláveis

Volver a los lenguajes
Elixir
76 tarjetas encontradas
Categorías:
Versiones:

Recursos Avançados


8 cards
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"
end

Un 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)
end

El 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
end

El 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
# => 4

Un 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"]
end

Los 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 nombres

El 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


10 cards
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 siguiente

El 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>>      # bitstring

Elixir 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-8

Las 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


9 cards
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)    # 10

Las 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=20

El 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}"
end

El 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


9 cards
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")    # 4

Un 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 funciones

Una 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)   # true

Los 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 tipo

Un 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 devuelve

Para 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


9 cards
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


9 cards
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
end

Los 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
end

La 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)
end

El 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


8 cards
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}"
end

El 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, KeyError

El 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)   # 120

La 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 else

El 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


7 cards
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
end

Un 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
end

Una 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)}")
end

Un 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


7 cards
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
end

El 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.Servidor

Un 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
end

El ExUnit es el framework de tests. Los tests usan test con assert/refute y corren con mix test. El assert_raise verifica excepciones.