cv.cm

Programación · Lenguajes de programación

Tutorial de Elixir para principiantes

8 min de lectura

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 regex

Qué 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 tool

Los 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)
TipoEjemploNotas
integer42, 1_000_000precisión arbitraria
float3.14doble precisión de 64 bits
atom:ok, :errorconstantes; 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 value

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

with 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: 2

Siguientes 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.

Todos los tutoriales de programación