Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Elixir
Code

Fundamentos de la metaprogramación del elixir

by
Length:LongLanguages:

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

La metaprogramación es una técnica poderosa pero bastante compleja, lo que significa que un programa puede analizarse o incluso modificarse durante el tiempo de ejecución. Muchos idiomas modernos son compatibles con esta función, y Elixir no es una excepción.

Con la metaprogramación, puede crear nuevas macros complejas, definir y diferir dinámicamente la ejecución del código, lo que le permite escribir código más conciso y poderoso. De hecho, este es un tema avanzado, pero esperamos que después de leer este artículo obtenga una comprensión básica de cómo comenzar con la metaprogramación en Elixir.

En este artículo aprenderás:

  • Qué es el árbol de sintaxis abstracta y cómo se representa el código de Elixir bajo el capó.
  • Cuáles son las funciones quote y unquote.
  • Qué son las macros y cómo trabajar con ellas.
  • Cómo inyectar valores con enlace.
  • Por qué las macros son higiénicas.

Antes de comenzar, sin embargo, déjame darte un pequeño consejo. ¿Recuerdas que el tío de Spider Man dijo "Con gran poder viene una gran responsabilidad"? Esto también se puede aplicar a la metaprogramación porque es una característica muy poderosa que te permite girar y doblar el código a tu voluntad.

Aún así, no debe abusar de él, y debe atenerse a soluciones más simples cuando sea razonable y posible. Demasiada metaprogramación puede hacer que tu código sea mucho más difícil de entender y mantener, así que ten cuidado.

Arbol de Sintaxis abstracta y Quote

Lo primero que debemos comprender es cómo se representa realmente nuestro código de Elixir. Estas representaciones a menudo se llaman árboles de sintaxis abstracta (AST), pero la guía oficial de Elixir recomienda llamarlas simplemente expresiones citadas.

Parece que las expresiones vienen en forma de tuplas con tres elementos. Pero, ¿cómo podemos probar eso? Bueno, hay una función llamada quote que devuelve una representación para un código dado. Básicamente, hace que el código se convierta en una forma no evaluada. Por ejemplo:

Entonces, ¿qué está pasando aquí? La tupla devuelta por la función quote siempre tiene los siguientes tres elementos:

  1. Atom u otra tupla con la misma representación. En este caso, es un átomo :+, lo que significa que estamos realizando una suma. Por cierto, esta forma de operaciones de escritura debería ser familiar si has venido del mundo Ruby.
  2. Lista de palabras clave con metadatos. En este ejemplo, vemos que el módulo Kernel fue importado automáticamente para nosotros.
  3. Lista de argumentos o un átomo. En este caso, esta es una lista con los argumentos 1 y 2.

La representación puede ser mucho más compleja, por supuesto:

Por otro lado, algunos literales se devuelven cuando se citan, específicamente:

  • atoms
  • integers
  • floats
  • lists
  • strings
  • Tuples (¡pero solo con dos elementos!)

En el siguiente ejemplo, podemos ver que citar un átomo devuelve este átomo:

Ahora que sabemos cómo se representa el código bajo el capó, pasemos a la siguiente sección y veamos qué son las macros y por qué las expresiones citadas son importantes.

Macros

Las macros son formas especiales como funciones, pero las que devuelven el código entre comillas. Este código se coloca en la aplicación y se aplaza su ejecución. Lo interesante es que las macros tampoco evalúan los parámetros que se les pasan, también se representan como expresiones citadas. Las macros se pueden usar para crear funciones personalizadas y complejas que se usan en todo el proyecto.

Sin embargo, tenga en cuenta que las macros son más complejas que las funciones normales, y la guía oficial establece que solo deben usarse como último recurso. En otras palabras, si puede emplear una función, no cree una macro porque de esta manera su código se vuelve innecesariamente complejo y, de hecho, más difícil de mantener. Aún así, las macros tienen sus casos de uso, así que veamos cómo crear uno.

Todo comienza con la llamada a defmacro (que en realidad es una macro en sí):

Esta macro simplemente acepta un argumento y lo imprime.

Además, vale la pena mencionar que las macros pueden ser privadas, al igual que las funciones. Las macros privadas solo se pueden llamar desde el módulo donde se definieron. Para definir tal macro, utilice defmacrop.

Ahora vamos a crear un módulo separado que se utilizará como nuestro patio de recreo:

Cuando ejecute este código, se imprimirá {:{}, [line: 11], [1, 2, 3]} lo que de hecho significa que el argumento tiene un formato entre comillas (sin evaluar). Antes de continuar, sin embargo, déjame hacer una pequeña nota.

Requerir

¿Por qué en el mundo creamos dos módulos separados: uno para definir una macro y otro para ejecutar el código de muestra? Parece que tenemos que hacerlo de esta manera, porque las macros se procesan antes de que se ejecute el programa. También debemos asegurarnos de que la macro definida esté disponible en el módulo, y esto se hace con la ayuda de require. Esta función, básicamente, se asegura de que el módulo dado se compile antes que el actual.

Podría preguntar, ¿por qué no podemos deshacernos del módulo principal? Intentemos hacer esto:

Desafortunadamente, recibimos un error que indica que no se puede encontrar la prueba de función, aunque hay una macro con el mismo nombre. Esto sucede porque el módulo MyLib está definido en el mismo ámbito (y el mismo archivo) donde intentamos usarlo. Puede parecer un poco extraño, pero por ahora solo recuerde que se debe crear un módulo separado para evitar tales situaciones.

También tenga en cuenta que las macros no se pueden usar globalmente: primero debe importar o requerir el módulo correspondiente.

Macros y expresiones Quoted

Así que sabemos cómo se representan internamente las expresiones de Elixir y qué macros son ... ¿Y ahora qué? Bueno, ahora podemos utilizar este conocimiento y ver cómo se puede evaluar el código citado.

Volvamos a nuestras macros. Es importante saber que se espera que la última expresión de cualquier macro sea un código entre comillas que se ejecutará y devolverá automáticamente cuando se llame a la macro. Podemos volver a escribir el ejemplo de la sección anterior moviendo IO.inspect al módulo Main:

¿Mira qué pasa? ¡La tupla devuelta por la macro no se cita sino que se evalúa! Puedes intentar agregar dos enteros:

Una vez más, se ejecutó el código y se devolvieron 3. Incluso podemos intentar usar la función quote directamente, y la última línea aún será evaluada:

El arg fue citado (tenga en cuenta, por cierto, que incluso podemos ver el número de línea donde se llamó la macro), pero la expresión citada con la tupla {1,2,3} se evaluó para nosotros ya que esta es la última línea de la macro.

Podemos tener la tentación de intentar usar arg en una expresión matemática:

Pero esto generará un error diciendo que arg no existe. ¿Porque? Esto se debe a que arg se inserta literalmente en la cadena que citamos. Pero lo que nos gustaría hacer en su lugar es evaluar el arg, insertar el resultado en la cadena y luego realizar la cita. Para hacer esto, necesitaremos otra función llamada unquote.

Anulando el código

Unquote es una función que inyecta el resultado de la evaluación del código dentro del código que luego se citara. Esto puede sonar un poco extraño, pero en realidad las cosas son bastante simples. Vamos a ajustar el ejemplo del código anterior:

Ahora nuestro programa va a devolver 4, que es exactamente lo que queríamos! Lo que sucede es que el código que se pasa a la función unquote se ejecuta solo cuando se ejecuta el código citado, no cuando se analiza inicialmente.

Veamos un ejemplo un poco más complejo. Supongamos que nos gustaría crear una función que ejecute alguna expresión si la cadena dada es un palíndromo. Podríamos escribir algo como esto:

El sufijo _f aquí significa que esta es una función, más adelante crearemos una macro similar. Sin embargo, si intentamos ejecutar esta función ahora, el texto se imprimirá aunque la cadena no sea un palíndromo:

Los argumentos pasados a la función se evalúan antes de que se llame realmente a la función, por lo que vemos la cadena de "yes" impresa en la pantalla. De hecho, esto no es lo que queremos lograr, así que intentemos usar una macro en su lugar:

Aquí estamos citando el código que contiene la condición if y usamos unquote adentro para evaluar los valores de los argumentos cuando se llama la macro. En este ejemplo, no se imprimirá nada en la pantalla, ¡lo que es correcto!

Inyectando valores con enlaces

El uso de unquote no es la única forma de inyectar código en un bloque entre comillas. También podemos utilizar una característica llamada binding. En realidad, esto es simplemente una opción que se pasa a la función quote que acepta una lista de palabras clave con todas las variables que deben estar sin comillas una sola vez.

Para realizar el enlace, pase bind_quoted a la función quote como esta:

Esto puede ser útil cuando desea que la expresión utilizada en varios lugares se evalúe solo una vez. Como lo demuestra este ejemplo, podemos crear una macro simple que genere una cadena dos veces con un retraso de dos segundos:

Ahora, si lo llama pasando la hora del sistema, las dos líneas tendrán el mismo resultado:

Este no es el caso entre unquote, porque el argumento se evaluará dos veces con un pequeño retraso, por lo que los resultados no son los mismos:

Convertir código Quoted

A veces, es posible que desee comprender cómo se ve su código quoted para depurarlo, por ejemplo. Esto se puede hacer usando la función to_string:

La cadena impresa será:

Podemos ver que el argumento str dado fue evaluado, y el resultado fue insertado directamente en el código. \n aquí significa "nueva línea".

Además, podemos expandir el código entre comillas utilizando expand_once y expand:

Lo que produce:

Por supuesto, esta representación citada se puede volver a convertir en una cadena:

Obtendremos el mismo resultado que antes:

La función expand es más compleja, ya que intenta expandir cada macro en un código dado:

El resultado será:

Vemos esta salida porque if es realmente una macro en sí misma que se basa en la declaración case, por lo que también se expande.

En estos ejemplos, __ENV__ es una forma especial que devuelve información del entorno como el módulo, archivo, línea, variable actual en el alcance actual e importaciones.

Las macros son higiénicas

Es posible que hayas oído que las macros son realmente higiénicas. Lo que esto significa es que no sobrescriben ninguna variable fuera de su alcance. Para probarlo, agreguemos una variable de muestra, intente cambiar su valor en varios lugares, y luego imprímala:

¡Así que other_var recibió un valor dentro de Función start! , dentro de la macro, y dentro de quote. Verás la siguiente salida:

Esto significa que nuestras variables son independientes, y no estamos introduciendo ningún conflicto al usar el mismo nombre en todas partes (aunque, por supuesto, sería mejor evitar este enfoque).

Si realmente necesita cambiar la variable externa desde una macro, puede utilizar var! Me gusta esto:

Al usar var!, efectivamente estamos diciendo que la variable dada no debe ser higienizada. Sin embargo, tenga mucho cuidado al usar este enfoque, ya que puede perder la pista de lo que se está sobrescribiendo en qué lugar.

Conclusión

En este artículo, hemos discutido los conceptos básicos de metaprogramación en el lenguaje Elixir. Hemos cubierto el uso de quote, unquote, macros y enlaces mientras vemos algunos ejemplos y casos de uso. En este punto, está listo para aplicar este conocimiento en la práctica y crear programas más concisos y potentes. Sin embargo, recuerde que generalmente es mejor tener un código comprensible que un código conciso, por lo que no debe abusar de la metaprogramación en sus proyectos.

Si desea obtener más información acerca de las funciones que he descrito, siéntase libre de leer la guía de introducción inicial sobre macros, quote y unquote. Realmente espero que este artículo le haya brindado una buena introducción a la metaprogramación en Elixir, que de hecho puede parecer bastante compleja al principio. En cualquier caso, ¡no tenga miedo de experimentar con estas nuevas herramientas!

Te agradezco por estar conmigo, y hasta pronto.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.