Elixir está equipado con una infraestructura macro sofisticada y muy bien diseñada. Con la mano ligera de Chris McCord, hay una ley no escrita en la comunidad que inevitablemente se expresa tan pronto como se trata de macros: "La primera regla para usar macros es que no debes usarlas". A veces, con un comentario discreto escrito en una fuente gris pálido del cuarto tamaño: "solo si no puedes evitarlo y entiendes muy bien lo que vas a hacer y lo que estás arriesgando". Esto se debe al hecho de que las macros tienen acceso a todo el AST del módulo en el que se utilizan y, en general, pueden cambiar el código resultante más allá del reconocimiento.
En principio, estoy de acuerdo en que no debe usar macros en el proceso de familiarización con el lenguaje. Hasta ahora, no puede, al ser despertado a las tres de la mañana con una resaca, responder la pregunta de si este código se ejecuta en la etapa de compilación o en tiempo de ejecución. Elixir es un lenguaje compilado, y durante el proceso de compilación se ejecuta el código de "nivel superior", el árbol de sintaxis se expande completamente hasta que nos encontramos en una situación en la que no hay nada más que abrir, y este resultado finalmente se compila en BEAM . Cuando el compilador encuentra una llamada de macro en el código fuente, expone completamente el AST y se interrumpe en lugar de la llamada real . Es imposible de entender, solo se puede recordar.
Pero tan pronto como nos sintamos libres en la sintaxis, inevitablemente querremos usar todo el poder del conjunto de herramientas; aquí sin macros, en ninguna parte. Lo principal es no abusar de él. Las macros pueden reducir drásticamente (a valores negativos) la cantidad de código repetitivo que se puede requerir, y proporcionan una forma natural y muy conveniente de manipular AST . Phoenix , Ecto y todas las bibliotecas notables usan macros muy fuertemente.
Lo anterior es cierto para cualquier biblioteca / paquete universal. En mi experiencia, los proyectos ordinarios probablemente no son macros necesarios, o se necesitan en un área de aplicación muy limitada. Las bibliotecas, por el contrario, a menudo consisten en macros en una proporción de 80/20 al código regular.
No voy a hacer un cajón de arena aquí y esculpir magdalenas de macros para aquellos que no están muy conscientes de lo que comen; Si es interesante comenzar a aprender de los conceptos básicos, comprender qué es en principio, o cómo y por qué se usan en el propio Elixir , es mejor cerrar de inmediato esta página y leer el brillante libro Metaprogramming Elixir de Chris McCord, creador del Phoenix Framework . Solo quiero demostrar algunos trucos para mejorar un macro-ecosistema existente.
Las macros están intencionalmente mal documentadas. Este cuchillo es demasiado afilado para publicitarlo para niños.
Hay dos formas de usar macros. Lo más simple es que le indique al compilador que este módulo usará macros de otro usando la directiva Kernel.SpecialForms.require/2
, y llamará a la macro en sí después de eso (para las macros definidas en el mismo módulo, no require
necesario un require
explícito). En este artículo estamos interesados en otra forma más compleja. Cuando las llamadas de código externo use MyLib
, se espera que nuestro módulo MyLib
implemente la __using__/1
, que el compilador use MyLib
cuando encuentre el use MyLib
. Azúcar sintáctico, sí. Convención sobre configuración. El pasado ferroviario de José no pasó sin dejar rastro.
Atención: si el párrafo anterior te resulta desconcertante y todo lo anterior suena ridículo, deja de comer este cactus y lee el libro que mencioné anteriormente en lugar de mi nota de torta dulce.
__using__/1
toma un argumento, por lo que el propietario de la biblioteca puede permitir que los usuarios le pasen algunos parámetros. Aquí hay un ejemplo de uno de mis proyectos internos que usa una llamada macro con parámetros:
defmodule User do use MyApp.ActiveRecord, repo: MyApp.Repo, roles: ~w|supervisor client subscriber|, preload: ~w|setting companies|a
Se pasará un argumento de tipo keyword()
a MyApp.ActiveRecord.__using__/1
, y allí lo uso para crear varios ayudantes para trabajar con este modelo. ( Nota: este código lleva mucho tiempo borracho porque ActiveRecord pierde en todos los aspectos las llamadas Ecto nativas).
A veces queremos limitar el uso de macros a un subconjunto de módulos (por ejemplo, permitir que se use solo en estructuras). Una comprobación explícita dentro de la __using__/1
no funcionará, como nos gustaría, porque durante la compilación del módulo no tenemos acceso a su __ENV__
(y lo estaría, estaba lejos de estar completo en el momento en que el compilador encontró una llamada __using__/1
Sería ideal realizar esta verificación después de que se complete la compilación.
No hay problema! Hay dos atributos de módulo que configuran exactamente eso. Bienvenido a visitarnos, queridas devoluciones de llamadas en tiempo de compilación .
Aquí hay un breve extracto de la documentación.
@after_compile
devolución de llamada inmediatamente después de compilar el módulo actual.
Acepta un módulo o tupla {module, function_name}
. La devolución de llamada debe tomar dos argumentos: el entorno del módulo y su código de bytes. Cuando solo se pasa un módulo como argumento, se supone que este módulo exporta la función __after_compile__/2
.
Las devoluciones de llamada registradas primero se ejecutarán en último lugar.
defmodule MyModule do @after_compile __MODULE__ def __after_compile__(env, _bytecode) do IO.inspect env end end
No recomiendo inyectar __after_compile__/2
directamente en el código generado, ya que esto puede generar conflictos con las intenciones de los usuarios finales (que pueden querer usar sus propios controladores). Defina una función en algún lugar dentro de MyLib.Helpers
o algo así, y pase la tupla a @after_compile
:
quote location: :keep do @after_compile({MyLib.Helpers, :after_mymodule_callback}) end
Esta devolución de llamada se llamará inmediatamente después de compilar el módulo correspondiente, que utiliza nuestra biblioteca, y recibirá dos parámetros: la estructura __ENV__
y el __ENV__
del módulo compilado. Este último rara vez es utilizado por simples mortales; El primero proporciona todo lo que necesitamos. El siguiente es un ejemplo de cómo me protejo de tratar de llamar al use Iteraptable
desde módulos que no implementan estructuras. De hecho, el código de verificación simplemente llama desde la __struct__
de __struct__
__struct__ en el módulo compilado y el delegado cursi delega el derecho de lanzar una excepción con un texto claro que explique la causa del problema:
def struct_checker(env, _bytecode), do: env.module.__struct__
El código anterior arrojará una excepción si el módulo compilado no es una estructura. Por supuesto, el código de verificación puede ser mucho más complicado, pero la idea principal es si su módulo usado espera algo del módulo que lo usa . Si es así, tiene sentido no olvidarse de @after_compile
y maldecir desde allí si no se cumplen todas las condiciones necesarias. Lanzar una excepción es el enfoque correcto en este caso un poco más que siempre, ya que este código se ejecuta en la etapa de compilación.
Una historia divertida está relacionada con @after_callback
, que explica completamente por qué me encanta OSS en general y Elixir en particular. Hace aproximadamente un año, cometí un error al copiar y pegar y copié desde algún lugar @after_callback
lugar de @before_callback
. La diferencia entre ellos es probablemente obvia: el segundo se llama antes de la compilación , y desde allí todos pueden cambiar el árbol de sintaxis más allá del reconocimiento. Y yo, oh, cómo, lo cambié. Pero esto no condujo a ningún resultado en el código compilado: no cambió en absoluto. Después de tres tazas de café, noté un error tipográfico, reemplacé after
por before
y todo comenzó; pero la pregunta me atormentaba: ¿por qué el compilador guardó silencio, como un partisano? Resultó que Module.open?/1
devuelve true
de esta devolución de llamada (que, en principio, no está lejos de la verdad: el módulo todavía no está realmente cerrado, el acceso a sus atributos no está cerrado y muchas bibliotecas usan este error no documentado).
Bueno, bosquejé una solución, envié una solicitud de extracción a la corteza del idioma (al compilador, si es absolutamente estricto), y menos de un día después, terminó en el maestro .
Entonces fue cuando necesitaba la configuración de usuario en IO.inspect/2
, y en algunos casos. ¿Qué pasaría si me tropiezo con esto en Java? - Da miedo imaginarlo.
Que tengas una buena macro!