Programación · Lenguajes de programación
Tutorial de Elixir para principiantes
Respuesta corta
Elixir es un lenguaje dinámico y funcional que se ejecuta en la máquina virtual de Erlang (BEAM), diseñado para sistemas escalables, tolerantes a fallos y concurrentes. Sus ideas centrales son los datos inmutables, la coincidencia de patrones, el operador pipe y los procesos ligeros que se comunican mediante mensajes. Esta página cubre la configuración, los tipos, la coincidencia de patrones, el control de flujo, los módulos y funciones, las colecciones, los procesos y el manejo de errores.
Abrir Probador de regexQué es Elixir y para qué se usa
Elixir es un lenguaje dinámico y funcional creado por José Valim y lanzado por primera vez en 2012. Se ejecuta en BEAM, la máquina virtual de Erlang, por lo que hereda los procesos ligeros, la tolerancia a fallos y el soporte para sistemas distribuidos de Erlang, y añade una sintaxis amigable, macros y herramientas modernas.
Elixir se usa para aplicaciones web y APIs con el framework Phoenix, funciones en tiempo real como chats y paneles en vivo (Phoenix LiveView), sistemas de mensajería, pipelines de datos, dispositivos embebidos con Nerves y aprendizaje automático con Nx. Las empresas lo eligen para servicios que deben manejar muchas conexiones concurrentes y seguir funcionando cuando fallan algunas partes.
Instala Elixir y ejecuta Hola, mundo
Instala Elixir desde elixir-lang.org (también instala Erlang/OTP o lo requiere). Los scripts usan la extensión .exs; los archivos de proyecto compilados usan .ex. iex inicia la shell interactiva.
# hello.exs
IO.puts("Hello, world!")elixir hello.exs
# Hello, world!
iex # interactive shell; type h(Enum.map) for help
mix new my_app # create a new project with Mix, the build toolLos comentarios empiezan con #. Los paréntesis alrededor de los argumentos de una función son opcionales en muchos casos, así que IO.puts "hi" también funciona.
Tipos básicos e inmutabilidad
Todos los datos de Elixir son inmutables. Volver a enlazar un nombre con = lo apunta a un valor nuevo; nunca cambia el anterior. Las cadenas son binarios UTF-8 escritos entre comillas dobles, y los átomos son constantes con nombre que empiezan con dos puntos.
name = "Elixir"
year = 2012
IO.puts("#{name} since #{year}") # Elixir since 2012
IO.puts(10 / 4) # 2.5 (always a float)
IO.puts(div(10, 4)) # 2
IO.puts(rem(10, 4)) # 2
IO.puts("ab" <> "cd") # abcd (string concatenation)
IO.puts(String.length("héllo")) # 5 (counts characters, not bytes)
IO.inspect(:ok) # :ok
IO.inspect(is_atom(true)) # true (booleans are atoms)| Tipo | Ejemplo | Notas |
|---|---|---|
| integer | 42, 1_000_000 | precisión arbitraria |
| float | 3.14 | doble precisión de 64 bits |
| atom | :ok, :error | constantes; true, false y nil son átomos |
| string | "hello" | binario UTF-8; interpolación con #{} |
| list | [1, 2, 3] | lista enlazada |
| tuple | {:ok, 1} | tamaño fijo, almacenada de forma contigua |
| map | %{"a" => 1} | almacén clave-valor |
Coincidencia de patrones
En Elixir, = es el operador de coincidencia. El lado izquierdo es un patrón; si el lado derecho encaja, las variables del patrón quedan enlazadas. La coincidencia de patrones se usa en todas partes: en asignaciones, en case, en cabeceras de funciones y en el manejo de errores.
{:ok, value} = {:ok, 42}
IO.puts(value) # 42
[first | rest] = [1, 2, 3]
IO.inspect({first, rest}) # {1, [2, 3]}
%{name: n} = %{name: "Ann", age: 31}
IO.puts(n) # Ann
result = {:error, :not_found}
message =
case result do
{:ok, v} -> "got #{v}"
{:error, reason} -> "failed: #{reason}"
end
IO.puts(message) # failed: not_found
x = 1
^x = 1 # pin operator: match the current valueSi un patrón no coincide, Elixir lanza un MatchError. El operador pin ^ significa «usa el valor actual de esta variable» en lugar de volver a enlazarla.
Control de flujo
Elixir tiene case, cond e if/unless, y todos devuelven valores. No hay bucles for/while que modifiquen contadores; en su lugar se usan la recursión, el módulo Enum o la comprensión for.
temp = 22
feel =
cond do
temp > 30 -> "hot"
temp >= 15 -> "mild"
true -> "cold"
end
IO.puts(feel) # mild
IO.puts(if(rem(temp, 2) == 0, do: "even", else: "odd")) # even
squares = for n <- 1..5, rem(n, 2) == 1, do: n * n
IO.inspect(squares) # [1, 9, 25]Solo false y nil son falsos; todo lo demás, incluidos 0 y "", es verdadero.
Funciones, módulos y el operador pipe
Las funciones con nombre viven en módulos definidos con defmodule. Una función puede tener varias cláusulas, elegidas mediante coincidencia de patrones y guardas (when). Las funciones anónimas se escriben fn ... end y se llaman con un punto. El operador pipe |> pasa el resultado de una expresión como primer argumento de la siguiente llamada.
defmodule Math do
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
def sign(n) when n < 0, do: :negative
def sign(0), do: :zero
def sign(_n), do: :positive
end
IO.puts(Math.factorial(5)) # 120
IO.inspect(Math.sign(-3)) # :negative
double = fn x -> x * 2 end
IO.puts(double.(21)) # 42 (note the dot)
" Elixir is fun "
|> String.trim()
|> String.upcase()
|> String.split()
|> IO.inspect() # ["ELIXIR", "IS", "FUN"]Las funciones se identifican por su nombre y su aridad (número de argumentos), que en la documentación se escriben como String.split/1 o Math.factorial/1.
Colecciones: listas, mapas, listas de palabras clave y Enum
El módulo Enum funciona con cualquier colección enumerable y ofrece map, filter, reduce, sort_by, group_by y muchas más. Los mapas son la principal estructura clave-valor; las listas de palabras clave, como [size: 10, color: :red], se usan habitualmente para opciones.
nums = [5, 3, 8, 1]
IO.inspect(Enum.sort(nums)) # [1, 3, 5, 8]
IO.inspect(Enum.map(nums, &(&1 * 10))) # [50, 30, 80, 10]
IO.inspect(Enum.filter(nums, fn n -> n > 2 end)) # [5, 3, 8]
IO.puts(Enum.reduce(nums, 0, &+/2)) # 17
user = %{name: "Ann", age: 31}
older = %{user | age: 32} # update an existing key
IO.puts(user.age) # 31 (unchanged)
IO.puts(older.age) # 32
with_city = Map.put(user, :city, "Oslo")
IO.puts(with_city.city) # Oslo
IO.inspect(Map.get(user, :email, "none")) # "none"El operador de captura & crea funciones anónimas cortas: &(&1 * 10) significa fn x -> x * 10 end, y &+/2 se refiere a la función + con dos argumentos.
Procesos y concurrencia
La característica distintiva de Elixir es el proceso de BEAM: una unidad de ejecución aislada y muy ligera con su propia memoria. Los procesos se comunican únicamente enviando mensajes, y una sola máquina puede ejecutar cientos de miles. Los supervisores reinician los procesos que fallan, que es la base del enfoque «let it crash» de Elixir para la tolerancia a fallos.
parent = self()
spawn(fn ->
send(parent, {:result, 6 * 7})
end)
receive do
{:result, value} -> IO.puts("Got #{value}") # Got 42
after
1_000 -> IO.puts("timeout")
end
tasks = for n <- 1..3, do: Task.async(fn -> n * n end)
IO.inspect(Task.await_many(tasks)) # [1, 4, 9]En las aplicaciones reales rara vez llamas a spawn directamente. Usas abstracciones de OTP como Task, GenServer, Agent y Supervisor.
Manejo de errores
El Elixir idiomático devuelve tuplas etiquetadas como {:ok, value} o {:error, reason} para los fallos esperados y usa case o with para tratarlos. Las funciones cuyo nombre termina en !, como File.read!/1, lanzan una excepción. Las excepciones pueden capturarse con try/rescue, pero no es lo habitual.
case File.read("missing.txt") do
{:ok, text} -> IO.puts(text)
{:error, reason} -> IO.puts("Cannot read: #{reason}") # Cannot read: enoent
end
with {a, ""} <- Integer.parse("40"),
{b, ""} <- Integer.parse("2") do
IO.puts(a + b) # 42
else
_ -> IO.puts("not a whole number")
end
try do
String.to_integer("abc")
rescue
e in ArgumentError -> IO.puts("Error: #{Exception.message(e)}")
endwith encadena varias coincidencias y se detiene en la primera que no coincide, devolviendo ese valor. Evita expresiones case anidadas en exceso.
Un programa pequeño y completo: frecuencia de palabras
Este script cuenta las palabras de un texto e imprime las tres más comunes. Es un pipeline típico de Elixir construido con funciones de String y Enum.
defmodule WordFreq do
def top(text, n) do
text
|> String.downcase()
|> String.split(~r/[^a-z']+/, trim: true)
|> Enum.frequencies()
|> Enum.sort_by(fn {word, count} -> {-count, word} end)
|> Enum.take(n)
end
end
"The cat and the dog. The dog sat, and the cat ran."
|> WordFreq.top(3)
|> Enum.each(fn {word, count} -> IO.puts("#{word}: #{count}") end)
# the: 4
# and: 2
# cat: 2Siguientes pasos
- La guía oficial Getting Started en hexdocs.pm y el sitio web elixir-lang.org.
- HexDocs, donde se publica la documentación de Elixir y de cada paquete de Hex.
- Las guías del framework Phoenix para el desarrollo web.
- Elixir School, un plan de estudios gratuito de la comunidad, y el itinerario de Elixir de Exercism para practicar.
Preguntas frecuentes
¿Cuál es la diferencia entre Elixir y Erlang?
Ambos se ejecutan en la misma máquina virtual BEAM y pueden llamar al código del otro. Elixir añade una sintaxis inspirada en Ruby, macros, el operador pipe y herramientas como Mix y Hex, mientras que Erlang es el lenguaje más antiguo con el que se construyó OTP.
¿Elixir es bueno para el desarrollo web?
Sí. El framework Phoenix es rápido y maneja muchas conexiones simultáneas, y LiveView permite crear páginas interactivas con HTML renderizado en el servidor y poco JavaScript.
¿Elixir tiene tipado estático?
Elixir tiene tipado dinámico. Las versiones recientes están incorporando al compilador un sistema de tipos gradual basado en teoría de conjuntos, y muchos equipos usan además Dialyzer con typespecs para encontrar errores de tipos.
¿Qué significa |> en Elixir?
|> es el operador pipe. x |> f(y) equivale a f(x, y), de modo que una cadena de transformaciones se puede leer de arriba abajo.
¿Qué son los archivos .ex y .exs?
Los archivos .ex se compilan como parte de un proyecto, mientras que los .exs son scripts que se interpretan al ejecutarse y se usan habitualmente para pruebas y configuración.