Fundamentos de la metaprogramación del elixir

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 característica, 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. 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ó.
  • Que citar y entre comillas las funciones son.
  • 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 le permite girar y doblar el código a su 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..

Árbol de sintaxis abstracta y Citar

Lo primero que debemos comprender es cómo se representa realmente nuestro código de Elixir. Estas representaciones a menudo se denominan á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 citar que devuelve una representación para algún código dado. Básicamente, hace que el código se convierta en un forma no evaluada. Por ejemplo:

quote do 1 + 2 end # => : +, [context: Elixir, import: Kernel], [1, 2]

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

  1. Atom u otra tupla con la misma representación. En este caso, es un átomo. :+, Es decir, estamos realizando la adición. 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 Núcleo módulo fue importado para nosotros automáticamente.
  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:

quote do Enum.each ([1,2,3], & (IO.puts (& 1))) final # => :., [], [: __ aliases__, [alias: false], [: Enum ],: each], [], # [[1, 2, 3], # : &, [], # [:., [], [: __ aliases__, [alias: false], [: IO],: pone], [], # [: &, [], [1]]]]

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

  • átomos
  • enteros
  • flotadores
  • liza
  • instrumentos de cuerda
  • Tuplas (pero solo con dos elementos!)

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

cita do: hi end # =>: hi

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 que es 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 a lo largo de su 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 el defmacro llamada (que en realidad es una macro en sí):

defmódulo MyLib do defmacro test (arg) do arg |> IO.inspect end end

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, use defmacrop.

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

Defmodule Main no requiere MyLib def start! do MyLib.test (1,2,3) finaliza Main.start final!

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

Exigir

¿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 exigir. Esta función, básicamente, asegura que el módulo dado se compila antes del actual.

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

defmodule MyLib do defmacro test (arg) do arg |> IO.inspect end end MyLib.test (1,2,3) # => ** (UndefinedFunctionError) función MyLib.test / 1 no está definido o es privado. Sin embargo hay una macro con el mismo nombre y aridad. Asegúrese de requerir MyLib si pretende invocar esta macro # MyLib.test (1, 2, 3) # (elixir) lib / code.ex: 376: Code.require_file / 2

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 MyLib El módulo se define en el mismo ámbito (y el mismo archivo) donde intentamos usarlo. Puede parecer un poco extraño, pero por ahora solo recuerde que debe crearse 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 citadas

Así que sabemos cómo se representan internamente las expresiones de Elixir y qué son las macros ... ¿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 la última expresión de cualquier macro se espera que sea un código entre comillas que se ejecutará y devolverá automáticamente cuando se llame a la macro. Podemos reescribir el ejemplo de la sección anterior moviendo IO.inspect al Principal módulo: 

defmodule MyLib do defmacro test (arg) do arg end end defmodule Main ¡requiere MyLib def start! haga MyLib.test (1,2,3) |> IO.inspect end end Main.start! # => 1, 2, 3

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

MyLib.test (1 + 2) |> IO.inspect # => 3

Una vez más, el código fue ejecutado, y 3 fue devuelto Incluso podemos intentar usar el citar Funciona directamente, y la última línea aún será evaluada:

defmodule MyLib do defmacro test (arg) do arg |> IO.inspect quote do 1,2,3 end end end end # ... def start! hacer MyLib.test (1 + 2) |> IO.inspect # => : +, [line: 14], [1, 2] # 1, 2, 3 end

los 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 fue evaluado para nosotros ya que esta es la última línea de la macro.

Podemos estar tentados a intentar usar el arg en una expresión matemática:

 prueba defmacro (arg) do quote do arg + 1 end end

Pero esto provocará un error diciendo que arg no existe. ¿Porque? Esto es porque arg Se inserta literalmente en la cadena que citamos. Pero lo que nos gustaría hacer es evaluar el arg, inserte el resultado en la cadena, y luego realice la cita. Para hacer esto, necesitaremos otra función llamada entre comillas.

Anulando el código

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

 defmacro test (arg) do quote do unquote (arg) + 1 end end

Ahora nuestro programa va a volver 4, que es exactamente lo que queríamos! Lo que pasa es que el código pasado a la entre comillas La función se ejecuta solo cuando se ejecuta el código entre comillas, 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:

 def if_palindrome_f? (str, expr) do if str == String.reverse (str), do: expr end

los _F El sufijo aquí significa que esta es una función, ya que 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:

 def empezar! do MyLib.if_palindrome_f? ("745", IO.puts ("yes")) # => "yes" end

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

 defmacro if_palindrome? (str, expr) do quote do if (unquote (str) == String.reverse (unquote (str))) unquote (expr) end end end end # ... MyLib.if_palindrome? ("745", IO. pone ("sí"))

Aquí estamos citando el código que contiene el Si condicion y uso entre comillas en el interior para evaluar los valores de los argumentos cuando se llama realmente a la macro. En este ejemplo, no se imprimirá nada en la pantalla, lo cual es correcto!

Inyectando valores con enlaces

Utilizando entre comillas no es la única forma de inyectar código en un bloque citado. También podemos utilizar una función llamada Unión. En realidad, esto es simplemente una opción pasada al citar Función que acepta una lista de palabras clave con todas las variables que deben estar sin comillas. sólo una vez.

Para realizar la encuadernación, pasar. bind_quoted al citar funciona así:

quote bind_quoted: [expr: expr] do end

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:

defmódulo MyLib do defmacro test (arg) do quote bind_quoted: [arg: arg] do arg |> IO.inspect Process.sleep 2000 arg |> IO.inspect end end end end

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

: os.system_time |> MyLib.test # => 1547457831862272 # => 1547457831862272

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

 defmacro test (arg) do quote do unquote (arg) |> IO.inspect Process.sleep (2000) unquote (arg) |> IO.inspect end end # ... def start! do: os.system_time |> MyLib.test # => 1547457934011392 # => 1547457936059392 end

Convertir código cotizado

A veces, es posible que desee comprender cómo se ve su código entre comillas para depurarlo, por ejemplo. Esto se puede hacer usando el Encadenar función:

 defmacro if_palindrome? (str, expr) do quoted = quote do if (unquote (str) == String.reverse (unquote (str))) do unquote (expr) end end end itoted |> Macro.to_string |> IO.inspect quoted fin

La cadena impresa será:

"if (\" 745 \ "== String.reverse (\" 745 \ ")) do \ n IO.puts (\" yes \ ") \ nend"

Podemos ver que lo dado str El argumento se evaluó y el resultado se insertó directamente en el código.. \norte aquí significa "nueva línea".

 Además, podemos expandir el código citado usando expand_once y expandir:

 def empezar! do quoted = quote do MyLib.if_palindrome? ("745", IO.puts ("yes")) fin entrecomillado |> Macro.expand_once (__ ENV__) |> IO.inspect end

Lo que produce:

: if, [context: MyLib, import: Kernel], [: ==, [context: MyLib, import: Kernel], ["745", :., [], [: __ aliases__, [alias : falso, contador: -576460752303423103], [: String],: reverse], [], ["745"]], [do: :., [], [: __ aliases__, [alias : falso, contador: -576460752303423103], [: IO],: puts], [], ["yes"]]]

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

citado |> Macro.expand_once (__ ENV__) |> Macro.to_string |> IO.inspect

Obtendremos el mismo resultado que antes:

"if (\" 745 \ "== String.reverse (\" 745 \ ")) do \ n IO.puts (\" yes \ ") \ nend"

los expandir La función es más compleja ya que trata de expandir cada macro en un código dado:

citado |> Macro.expand (__ ENV__) |> Macro.to_string |> IO.inspect

El resultado será:

"case (\" 745 \ "== String.reverse (\" 745 \ ")) do \ nx cuando x en [false, nil] -> \ n nil \ n _ -> \ n IO.puts (\" sí "

Vemos esta salida porque Si Es en realidad una macro en sí misma que se basa en el caso declaración, por lo que se expande también.

En estos ejemplos, __ENV__ es un formulario 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 en realidad higiénico. 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:

 defmacro if_palindrome? (str, expr) do other_var = "if_palindrome?" quoted = quote do other_var = "quoted" if (unquote (str) == String.reverse (unquote (str))) do unquote (expr) end other_var |> IO.inspect end other_var |> IO.inspect quoted end # com def empezar! do other_var = "empezar!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect end

Asi que otro_var se le dio un valor dentro de la comienzo! función, dentro de la macro, y dentro de la macro citar. Verás la siguiente salida:

"if_palindrome?" "citado" "empezar!"

Esto significa que nuestras variables son independientes, y no estamos introduciendo ningún conflicto usando 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:

 defmacro if_palindrome? (str, expr) do quoted = quote do var! (other_var) = "quoted" if (unquote (str) == String.reverse (unquote (str))) do unquote (expr) end fin entre comillas end # ... ¡de empezar! do other_var = "empezar!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect # => "quoted" end

Mediante el uso 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 donde.

Conclusión

En este artículo, hemos discutido los conceptos básicos de metaprogramación en el lenguaje Elixir. Hemos cubierto el uso de citar, entre comillas, Macros y enlaces al ver 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, por lo general, 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, comillas y sin comillas. Realmente espero que este artículo te haya dado una buena introducción a la metaprogramación en Elixir, que puede parecer bastante complejo al principio. En cualquier caso, no tenga miedo de experimentar con estas nuevas herramientas.!

Te agradezco por estar conmigo y hasta pronto..