Sobre las macros, require y use

Use es una palabra clave empleada para invocar una macro declarada en otro módulo con el objetivo de importar código en nuestro módulo. Como si fuese un copia y pega, se traerá definiciones que haya en ese módulo. Require sirve para importar macros específicas. Esto requiere que presente por encima la metaprogramación, aunque no es el día de hablar de ella.

Cuando trabajamos con Elixir, nos encontramos con conceptos que van más allá de las funciones y módulos tradicionales, como las macros y las palabras clave use y require. Estas herramientas nos permiten hacer metaprogramación, es decir, escribir código que se evalúa en tiempo de compilación y que puede modificar o generar código dinámicamente, facilitando la reutilización y la creación de lenguajes específicos dentro de nuestros proyectos.

Las macros en Elixir son fragmentos de código que no se ejecutan en tiempo de ejecución, sino que se evalúan durante la compilación. Esto significa que cuando compilamos nuestro código, las macros se expanden y sustituyen por otro código que el compilador entiende directamente. Por ejemplo, en los tests de Elixir, la palabra clave test no es una función común, sino una macro que, al compilar, se transforma en una función con def. Así, el compilador reemplaza la línea con test por una definición de función real, lo que nos permite escribir tests con una sintaxis más expresiva y específica.

Un caso muy común donde vemos macros es con la palabra clave use. Cuando escribimos use ExUnit.Case, lo que realmente sucede es que se invoca una macro llamada using dentro del módulo ExUnit.Case. Esta macro devuelve código que se inyecta directamente en nuestro módulo, como si lo copiáramos y pegáramos allí. Este código puede incluir funciones, macros y otras definiciones que nos permiten usar funcionalidades específicas, como las herramientas para escribir tests. Internamente, este código se representa como un AST (Abstract Syntax Tree) dentro de un bloque quote, que es la forma en que Elixir maneja el código como datos para la metaprogramación.

Por ejemplo, la macro test que importamos con use ExUnit.Case está definida para expandirse en una función def que contiene el cuerpo del test. Esto nos permite escribir tests con una sintaxis sencilla y legible, mientras que Elixir se encarga de transformar ese código en funciones que se ejecutan realmente.

Además de importar código, las macros y use nos permiten crear DSLs (Domain Specific Languages), que son pequeños lenguajes específicos para un dominio concreto. En Elixir, el DSL de ExUnit nos ofrece palabras clave como test, assert o describe, que no son palabras reservadas del lenguaje, pero que nuestro editor reconoce y resalta porque forman parte de este lenguaje específico para testing. Otros ejemplos de DSLs en Elixir los encontramos en librerías como Ecto, que también usan use para traer su propio conjunto de macros y funciones que facilitan la interacción con bases de datos.

Por otro lado, la palabra clave require tiene un uso más puntual y está relacionada con el uso de macros definidas en otros módulos. Cuando queremos usar una macro externa, como las guardas is_even o is_odd del módulo Integer, debemos primero hacer un require Integer. Esto es necesario porque las macros no se importan automáticamente y Elixir nos obliga a declarar explícitamente que queremos usarlas para evitar confusiones y mantener el código claro. Una vez hecho el require, podemos llamar a estas macros como si fueran funciones normales, pero recordemos que su evaluación ocurre en tiempo de compilación.

Por ejemplo, las macros is_even e is_odd están definidas para funcionar como guardas, lo que significa que pueden usarse en cláusulas when para condicionar la ejecución de funciones. Su implementación se basa en operaciones bit a bit para determinar si un número es par o impar, y al ser macros, se expanden en código que el compilador puede optimizar.

En resumen, use nos permite importar y expandir código de otros módulos mediante macros, facilitando la creación de DSLs y la reutilización eficiente de código. Mientras que require es necesario para traer macros externas a nuestro contexto y poder usarlas, especialmente cuando trabajamos con guardas o funcionalidades específicas definidas como macros. Estas herramientas son poderosas, pero debemos usarlas con cuidado, ya que la metaprogramación puede hacer que el código generado sea menos transparente y más difícil de seguir si no se entiende bien cómo funcionan las expansiones en tiempo de compilación.

Para ilustrar cómo funciona una macro que se expande en una función, podemos imaginar una definición simplificada de la macro test:

defmacro test(description, do: block) do
  quote do
    def unquote(String.to_atom(description))() do
      unquote(block)
    end
  end
end

Aquí, la macro test recibe una descripción y un bloque de código, y genera una función con el nombre basado en la descripción, que contiene el bloque del test. Esta es la esencia de cómo Elixir transforma el código que escribimos en algo que el compilador puede ejecutar directamente.

En definitiva, entender cómo funcionan las macros, use y require nos abre la puerta a aprovechar al máximo la metaprogramación en Elixir, creando código más expresivo, reutilizable y adaptado a nuestras necesidades específicas.

Lista de reproducción
  1. 1
    mix
    10 minutos
  2. 2
    Documentando código: comentarios, docs y moduledocs
    10 minutos
  3. 3
    Atributos de módulo
    9 minutos
  4. 4
    Dependencias
    12 minutos
  5. 5
    Un ejemplo práctico de módulo útil
    13 minutos
  6. 6
    Alias e import
    10 minutos
  7. 7
    Sobre las macros, require y use
    11 minutos
  8. 8
    Typespecs (parte 1, usando tipos básicos)
    10 minutos
  9. 9
    Typespecs (parte 2, tipos propios y t())
    11 minutos
  10. 10
    Comportamientos
    11 minutos
  11. 11
    Tratamiento de errores con rescue
    8 minutos
  12. 12
    Elevando errores con raise
    8 minutos
  13. 13
    with
    14 minutos
  14. 14
    Sigilos
    8 minutos
  15. 15
    Tests con ExUnit
    12 minutos
  16. 16
    Más particularidades de ExUnit
    13 minutos
  17. 17
    Microservicios en Elixir con Plug
    11 minutos
  18. 18
    Cómo Plug.Router te ayuda a escribir microservicios en Elixir
    14 minutos
  19. 19
    ¿Cómo hacer rutas dinámicas en Phoenix y Plug?
    13 minutos