Crochets Elixir au moment de la compilation

Elixir est équipé d'une macro-infrastructure sophistiquée et très bien conçue. Avec la main légère de Chris McCord, il existe une loi non écrite dans la communauté qui est inévitablement exprimée immédiatement en ce qui concerne les macros: "La première règle pour utiliser des macros est que vous ne devez pas les utiliser." Parfois avec une remarque subtile tapée dans une police gris pâle de la quatrième taille: "seulement si vous ne pouvez pas l'éviter, et que vous comprenez très bien ce que vous allez faire et ce que vous risquez." Cela est dû au fait que les macros ont accès à l'ensemble de l' AST du module dans lequel elles sont utilisées et, d'une manière générale, elles peuvent changer le code résultant au-delà de la reconnaissance.


En principe, je conviens que vous ne devez pas utiliser de macros dans le processus de familiarisation avec la langue. Jusqu'à présent, vous ne pouvez pas, étant réveillé à trois heures du matin avec une gueule de bois, répondre à la question de savoir si ce code est exécuté au stade de la compilation ou lors de l'exécution. Elixir est un langage compilé, et pendant le processus de compilation, le code de «haut niveau» est exécuté, l'arbre de syntaxe est complètement développé jusqu'à ce que nous nous trouvions dans une situation où il n'y a plus rien à ouvrir, et ce résultat est finalement compilé dans BEAM . Lorsque le compilateur rencontre un appel de macro dans le code source, il expose entièrement l' AST pour celui - ci et se bloque à la place de l'appel réel . Il est impossible de comprendre, on ne peut que s'en souvenir.


Mais dès que nous nous sentirons libres dans la syntaxe, nous voudrons inévitablement utiliser toute la puissance de la boîte à outils; ici sans macros - nulle part. L'essentiel est de ne pas en abuser. Les macros peuvent réduire considérablement (à des valeurs négatives) la quantité de code passe-partout qui peut être requise, et elles fournissent un moyen naturel et très pratique de manipuler l' AST . Phoenix , Ecto et toutes les bibliothèques notables utilisent très fortement les macros.


Ce qui précède est vrai pour toute bibliothèque / package universel. D'après mon expérience, les projets ordinaires ne sont probablement pas des macros nécessaires, ou sont nécessaires dans un domaine d'application très limité. Les bibliothèques, en revanche, se composent souvent de macros dans un rapport de 80/20 au code normal.


Je ne vais pas faire un bac à sable ici et sculpter des muffins de macros pour ceux qui ne sont pas très conscients de ce qu'ils mangent; s'il est intéressant de commencer à apprendre des bases, de comprendre de quoi il s'agit, ou comment et pourquoi elles sont utilisées dans Elixir lui-même, il est préférable de fermer immédiatement cette page et de lire le brillant livre Metaprogramming Elixir de Chris McCord, créateur du Phoenix Framework . Je veux juste montrer quelques astuces pour améliorer un macro-écosystème existant.




Les macros sont intentionnellement mal documentées. Ce couteau est trop tranchant pour faire de la publicité pour les enfants.


Il existe deux façons d'utiliser les macros. Le plus simple est que vous indiquez au compilateur que ce module utilisera les macros d'un autre à l'aide de la directive Kernel.SpecialForms.require/2 , et appellera la macro elle-même après cela (pour les macros définies dans le même module, une require explicite require pas nécessaire). Dans cet article, nous nous intéressons à une autre manière plus complexe. Lorsque les appels de code externe use MyLib , il est prévu que notre module MyLib implémente la __using__/1 , que le compilateur use MyLib lorsqu'il rencontrera use MyLib . Sucre syntaxique, oui. Convention sur la configuration. Le chemin de fer passé de José ne passa pas sans laisser de trace.


Attention: si le paragraphe ci-dessus vous intrigue et que tout ce qui précède vous semble ridicule, arrêtez de manger ce cactus et lisez le livre que j'ai mentionné ci-dessus à la place de ma note sablée.


__using__/1 prend un argument, donc le propriétaire de la bibliothèque peut autoriser les utilisateurs à lui passer certains paramètres. Voici un exemple de l'un de mes projets internes qui utilise un appel de macro avec des paramètres:


 defmodule User do use MyApp.ActiveRecord, repo: MyApp.Repo, roles: ~w|supervisor client subscriber|, preload: ~w|setting companies|a 

Un argument de type keyword() sera passé à MyApp.ActiveRecord.__using__/1 , et là je l'utilise pour créer divers assistants pour travailler avec ce modèle. ( Remarque: ce code a longtemps été bu car ActiveRecord perd à tous égards aux appels natifs Ecto ).




Parfois, nous voulons limiter l'utilisation des macros à un sous-ensemble de modules (par exemple, autoriser son utilisation uniquement dans les structures). Une vérification explicite à l'intérieur de l' __using__/1 ne fonctionnera pas, comme nous le souhaiterions, car pendant la compilation du module, nous n'avons pas accès à son __ENV__ (et ce serait - c'était loin d'être terminé au moment où le compilateur a rencontré un appel __using__/1 Il serait idéal d'effectuer cette vérification une fois la compilation terminée.


Pas de problème! Il existe deux attributs de module qui configurent exactement cela. Bienvenue à nous rendre visite, chers rappels de temps de compilation .


Voici un bref extrait de la documentation.


@after_compile rappel sera appelé immédiatement après la compilation du module actuel.

Accepte un module ou un tuple {module, function_name} . Le rappel lui-même doit prendre deux arguments: l'environnement du module et son bytecode. Lorsque seul un module est passé en argument, il est supposé que ce module exporte la fonction __after_compile__/2 existe.

Les rappels enregistrés en premier seront exécutés en dernier.
 defmodule MyModule do @after_compile __MODULE__ def __after_compile__(env, _bytecode) do IO.inspect env end end 

Je déconseille fortement d'injecter __after_compile__/2 directement dans le code généré, car cela peut entraîner des conflits avec les intentions des utilisateurs finaux (qui peuvent vouloir utiliser leurs propres gestionnaires). Définissez une fonction quelque part dans votre MyLib.Helpers ou quelque chose, et passez le tuple à @after_compile :


 quote location: :keep do @after_compile({MyLib.Helpers, :after_mymodule_callback}) end 



Ce rappel sera appelé immédiatement après la compilation du module correspondant, qui utilise notre bibliothèque, et recevra deux paramètres: la structure __ENV__ et le bytecode du module compilé. Ce dernier est rarement utilisé par de simples mortels; le premier fournit tout ce dont nous avons besoin. Ce qui suit est un exemple de la façon dont je me protège d'essayer d'appeler use Iteraptable depuis des modules qui use Iteraptable pas de structures. En fait, le code de vérification appelle simplement depuis le __struct__ __struct__ sur le module compilé et délecte délègue Elixir le droit de lever une exception avec un texte clair expliquant la cause du problème:


 def struct_checker(env, _bytecode), do: env.module.__struct__ 

Le code ci-dessus lèvera une exception si le module compilé n'est pas une structure. Bien sûr, le code de vérification peut être beaucoup plus compliqué, mais l'idée principale est de savoir si votre module utilisé attend quelque chose du module qui l'utilise . Si c'est le cas, il est logique de ne pas oublier @after_compile et de malédiction à partir de là si toutes les conditions nécessaires ne sont pas remplies. Lancer une exception est la bonne approche dans ce cas un peu plus que toujours, car ce code est exécuté au stade de la compilation.




Une histoire amusante est liée à @after_callback , ce qui explique pleinement pourquoi j'aime OSS en général et Elixir en particulier. Il y a environ un an, j'ai fait une erreur de copier-coller et @after_callback copié quelque part @after_callback au lieu de @before_callback . La différence entre eux est probablement évidente: la seconde est appelée avant la compilation , et à partir de là, tout le monde peut changer l'arbre de syntaxe au-delà de la reconnaissance. Et je - oh, comment - je l'ai changé. Mais cela n'a conduit à aucun résultat dans le code compilé: cela n'a pas changé du tout. Après trois tasses de café, j'ai remarqué une faute de frappe, remplacée after par before et tout a commencé; mais la question me tourmentait: pourquoi le compilateur se taisait-il, comme un partisan. Il s'est avéré que Module.open?/1 renvoie true partir de ce rappel (qui, en principe, n'est pas loin de la vérité - le module n'est toujours pas fermé, l'accès à ses attributs n'est pas fermé, et de nombreuses bibliothèques utilisent ce bogue non documenté).


Eh bien, j'ai esquissé un correctif, envoyé une demande de traction à la croûte linguistique (au compilateur, si strictement strictement), et moins d'un jour plus tard, il s'est retrouvé dans le maître .


C'est donc lorsque j'ai eu besoin de paramètres utilisateur dans IO.inspect/2 , et dans certains cas. Que se passerait-il si je tombais dessus en Java? - C'est effrayant d'imaginer.




Ayez une belle macro!

Source: https://habr.com/ru/post/fr485810/


All Articles