Si hay una característica que distingue a Elixir de muchos otros lenguajes modernos, es su sistema de metaprogramación.
Elixir no solo permite escribir código que genera código: lo hace de una forma segura, estructurada y predecible gracias a sus macros higiénicas.
En un ecosistema donde las macros suelen ser sinónimo de caos (como en C o C++), Elixir logra que la metaprogramación sea una herramienta confiable, sin los típicos riesgos de colisión o ambigüedad.
Pero ¿cómo lo logra?
En muchos lenguajes, una macro es básicamente un reemplazo de texto: el compilador copia y pega fragmentos de código antes de compilar.
Ese enfoque funciona, pero también es peligroso.
Las macros del preprocesador de C, por ejemplo, no conocen el contexto del código, lo que puede generar errores difíciles de detectar:
#define square(x) x * x
int y = square(1 + 2); // => 1 + 2 * 1 + 2 => 5 (no 9)
Elixir, en cambio, hereda su modelo de Lisp, donde el código es una estructura de datos manipulable.
Eso significa que una macro no reemplaza texto, sino que transforma el árbol sintáctico abstracto (AST) del programa.
Pero Elixir va más allá: además de permitir manipular el AST, garantiza que las variables y nombres dentro de la macro no se mezclen accidentalmente con las del contexto donde se usa.
A eso se le llama higiene.
Una macro higiénica respeta el ámbito léxico del código.
Cuando definís una macro en Elixir, las variables que declares dentro de ella no interferirán con las variables del lugar donde se expanda la macro.
Veamos un ejemplo simple:
defmodule Ejemplo do
defmacro saludo do
quote do
nombre = "Mundo"
IO.puts("Hola " <> nombre)
end
end
end
nombre = "Elixir"
Ejemplo.saludo()
IO.puts(nombre)
Salida:
Hola Mundo
Elixir
A pesar de que tanto el código de la macro como el código del usuario usan la variable nombre, no hay conflicto.
Elixir “higieniza” los nombres internos durante la expansión para evitar interferencias.
Cada variable dentro del quote se convierte en una entidad única, ligada al módulo donde la macro fue definida, no al contexto donde se usa.
Elixir permite construir y manipular su propio AST a través de dos primitivas esenciales: quote y unquote.
- quote captura el código sin ejecutarlo, devolviendo su representación estructurada.
- unquote permite insertar expresiones evaluadas dentro de ese AST.
Por ejemplo:
expr = quote do: a + b
IO.inspect(expr)
# {:+, [context: Elixir, import: Kernel], [:a, :b]}
Una macro se define usando quote para construir el código que se insertará, y unquote para interpolar valores dentro de él.
El resultado es metaprogramación estructurada, no textual.
Esto significa que Elixir “entiende” el código que genera, lo valida, y lo protege contra colisiones de nombres.
Hay ocasiones en las que uno quiere que una macro interactúe con el contexto del usuario, por ejemplo, para definir variables o modificar el entorno del módulo.
En esos casos, Elixir permite romper la higiene intencionalmente con var! o con opciones explícitas en quote.
defmacro define_nombre do
quote do
var!(nombre) = "Definido desde macro"
end
end
define_nombre()
IO.puts(nombre)
# => "Definido desde macro"
Pero hacerlo implica responsabilidad: al romper la higiene, se pierde aislamiento.
Por eso, Elixir te obliga a hacerlo explícitamente —una decisión de diseño que mantiene el sistema seguro por defecto.
Mientras lenguajes como C ofrecen macros puramente textuales y Rust introduce un sistema de macros basado en AST pero sin aislamiento total, Elixir logra una síntesis elegante:
- la expresividad y poder de Lisp,
- la seguridad léxica del tipado moderno,
- y la claridad de un lenguaje que prioriza la legibilidad.
El resultado es un sistema de macros que permite extender el lenguaje sin riesgo de romperlo.
