Elixir-Hooks zur Kompilierungszeit

Elixir verfügt über eine ausgefeilte, sehr gut konzipierte Makro-Infrastruktur. Mit Chris McCords leichter Hand gibt es ein ungeschriebenes Gesetz in der Community, das unweigerlich ausgesprochen wird, sobald es um Makros geht: "Die erste Regel für die Verwendung von Makros ist, dass Sie sie nicht verwenden sollten." Manchmal mit einer subtilen Bemerkung in einer hellgrauen Schrift der vierten Größe: "Nur wenn Sie dies nicht vermeiden können und Sie sehr gut verstehen, was Sie tun und was Sie riskieren." Dies liegt an der Tatsache, dass Makros Zugriff auf den gesamten AST des Moduls haben, in dem sie verwendet werden, und im Allgemeinen den resultierenden Code bis zur Unkenntlichkeit ändern können.


Grundsätzlich bin ich damit einverstanden, dass Sie keine Makros verwenden, um sich mit der Sprache vertraut zu machen. Bisher können Sie nicht die Frage beantworten, ob dieser Code in der Kompilierungsphase oder zur Laufzeit ausgeführt wird, da Sie um drei Uhr morgens mit einem Kater geweckt werden. Elixir ist eine kompilierte Sprache, und während des Kompilierungsprozesses wird der Code der obersten Ebene ausgeführt, der Syntaxbaum wird vollständig erweitert, bis wir uns in einer Situation befinden, in der nichts mehr zu öffnen ist, und dieses Ergebnis wird schließlich in BEAM kompiliert. Wenn der Compiler im Quellcode auf einen Makroaufruf stößt, macht er den AST dafür vollständig verfügbar und stoppt anstelle des eigentlichen Aufrufs . Es ist unmöglich zu verstehen, es kann nur erinnert werden.


Sobald wir uns jedoch in der Syntax frei fühlen, werden wir unweigerlich die volle Leistungsfähigkeit des Toolkits nutzen wollen. hier ohne Makros - nirgendwo. Die Hauptsache ist, es nicht zu missbrauchen. Makros können die Menge des erforderlichen Kesselschild-Codes drastisch (auf negative Werte) reduzieren und bieten eine natürliche und sehr bequeme Möglichkeit, AST zu manipulieren. Phoenix , Ecto und alle namhaften Bibliotheken verwenden sehr häufig Makros.


Das oben Gesagte gilt für alle universellen Bibliotheken / Pakete. Nach meiner Erfahrung werden normale Projekte wahrscheinlich nicht als Makros oder in einem sehr begrenzten Anwendungsbereich benötigt. Im Gegensatz dazu bestehen Bibliotheken häufig aus Makros im Verhältnis 80/20 zu normalem Code.


Ich werde hier keinen Sandkasten einrichten und Makromuffins für diejenigen formen, die noch nicht genau wissen, was sie essen. Wenn es interessant ist, von den Grundlagen zu lernen, zu verstehen, was es im Prinzip ist oder wie und warum sie in Elixir selbst verwendet werden, ist es am besten, diese Seite sofort zu schließen und das brillante Buch Metaprogramming Elixir von Chris McCord, dem Schöpfer des Phoenix Framework, zu lesen. Ich möchte nur einige Tricks demonstrieren, um ein bestehendes Makro-Ökosystem besser zu machen.




Makros sind absichtlich schlecht dokumentiert. Dieses Messer ist zu scharf, um für Kinder zu werben.


Es gibt zwei Möglichkeiten, Makros zu verwenden. Am einfachsten ist es, wenn Sie den Compiler mithilfe der Kernel.SpecialForms.require/2 Direktive anweisen, dass dieses Modul Makros von einem anderen verwendet, und anschließend das Makro selbst aufrufen (für Makros, die in demselben Modul definiert sind, require keine explizite require erforderlich). In diesem Artikel interessieren wir uns für eine andere, kompliziertere Art und Weise. Wenn die externen use MyLib , wird erwartet, dass unser MyLib Modul das Makro __using__/1 implementiert, das der Compiler use MyLib wenn er use MyLib . Syntaktischer Zucker, ja. Konvention über Konfiguration. Die Eisenbahnlinie von Jose verlief nicht spurlos.


Achtung: Wenn der obige Absatz Sie verwirrt und sich alles lächerlich anhört, hören Sie bitte auf, diesen Kaktus zu essen, und lesen Sie das oben erwähnte Buch anstelle meiner Shortbread-Notiz.


__using__/1 ein Argument, sodass der Bibliotheksbesitzer Benutzern erlauben kann, einige Parameter an das Argument zu übergeben. Hier ist ein Beispiel aus einem meiner internen Projekte, das einen Makroaufruf mit Parametern verwendet:


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

Ein Argument vom Typ keyword() wird MyApp.ActiveRecord.__using__/1 an MyApp.ActiveRecord.__using__/1 und dort verwende ich es, um verschiedene Helfer für die Arbeit mit diesem Modell zu erstellen. ( Hinweis: Dieser Code wurde lange Zeit getrunken, da ActiveRecord in jeder Hinsicht auf native Ecto- Aufrufe verliert.)




Manchmal möchten wir die Verwendung von Makros auf eine Untergruppe von Modulen beschränken (z. B. zulassen, dass sie nur in Strukturen verwendet werden). Eine explizite Überprüfung innerhalb der __using__/1 Implementierung wird nicht funktionieren, wie wir möchten, da wir während des Kompilierens des Moduls keinen Zugriff auf dessen __ENV__ (und es wäre - es war noch lange nicht abgeschlossen, als der Compiler auf einen Aufruf stieß) __using__/1 Es ist ideal, diese Prüfung nach Abschluss der Kompilierung durchzuführen.


Kein Problem! Es gibt zwei Modulattribute, die genau das konfigurieren. Willkommen bei uns, liebe Rückrufe bei der Kompilierungszeit .


Hier ist ein kurzer Auszug aus der Dokumentation.


@after_compile Callback wird sofort nach dem Übersetzen des aktuellen Moduls aufgerufen.

Akzeptiert ein Modul oder Tupel {module, function_name} . Der Rückruf selbst muss zwei Argumente annehmen: die Modulumgebung und ihren Bytecode. Wenn nur ein Modul als Argument übergeben wird, wird davon ausgegangen, dass dieses Modul die Funktion __after_compile__/2 exportiert.

Zuerst registrierte Rückrufe werden zuletzt ausgeführt.
 defmodule MyModule do @after_compile __MODULE__ def __after_compile__(env, _bytecode) do IO.inspect env end end 

Ich empfehle __after_compile__/2 direkt in den generierten Code __after_compile__/2 , da dies zu Konflikten mit den Absichten der Endbenutzer führen kann (die möglicherweise ihre eigenen Handler verwenden möchten). Definieren Sie eine Funktion irgendwo in Ihrer MyLib.Helpers oder so und übergeben Sie das Tupel an @after_compile :


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



Dieser Callback wird sofort nach dem Kompilieren des entsprechenden Moduls, das unsere Bibliothek verwendet, __ENV__ und erhält zwei Parameter: die __ENV__ Struktur und den Bytecode des kompilierten Moduls. Letzteres wird nur selten von Sterblichen benutzt; Das erste bietet alles, was wir brauchen. Das folgende Beispiel zeigt, wie ich mich use Iteraptable schützen kann, use Iteraptable von Modulen use Iteraptable , die keine Strukturen implementieren. Tatsächlich ruft der Bestätigungscode einfach vom __struct__- __struct__ des kompilierten Moduls aus auf, und kitschige Delegierte von Elixir haben das Recht, eine Ausnahme mit Klartext auszulösen, in der die Ursache des Problems erläutert wird:


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

Der obige Code löst eine Ausnahme aus, wenn das kompilierte Modul keine Struktur ist. Natürlich kann der Bestätigungscode viel komplizierter sein, aber die Hauptidee ist, ob Ihr verwendetes Modul etwas von dem Modul erwartet , das es verwendet . In diesem @after_compile ist es sinnvoll, @after_compile nicht zu vergessen und von dort aus zu verfluchen, wenn nicht alle erforderlichen Bedingungen erfüllt sind. Das Auslösen einer Ausnahme ist in diesem Fall etwas mehr als immer der richtige Ansatz, da dieser Code in der Kompilierungsphase ausgeführt wird.




Mit @after_callback ist eine lustige Geschichte verbunden, die vollständig erklärt, warum ich OSS im Allgemeinen und Elixir im Besonderen liebe. Vor ungefähr einem Jahr habe ich einen Fehler beim Kopieren und Einfügen gemacht und irgendwo von @after_callback anstelle von @before_callback . Der Unterschied zwischen ihnen ist wahrscheinlich offensichtlich: Der zweite wird vor dem Kompilieren aufgerufen, und von dort aus kann jeder den Syntaxbaum bis zur Unkenntlichkeit ändern. Und ich - oh, wie - ich habe es geändert. Dies führte jedoch zu keinen Ergebnissen im kompilierten Code: Es änderte sich überhaupt nicht. Nach drei Tassen Kaffee bemerkte ich einen Tippfehler, der after vorherigen ersetzt wurde, und alles begann. Aber die Frage quälte mich: Warum hat der Compiler geschwiegen, wie ein Partisan? Es stellte sich heraus, dass Module.open?/1 von diesem Rückruf Module.open?/1 zurückgibt (was im Prinzip nicht weit von der Wahrheit entfernt ist - das Modul ist immer noch nicht geschlossen, der Zugriff auf seine Attribute ist nicht geschlossen, und viele Bibliotheken verwenden diesen undokumentierten Fehler).


Nun, ich skizzierte einen Fix, schickte eine Pull-Anfrage an die Sprachenkruste (an den Compiler, wenn auch absolut streng) und weniger als einen Tag später landete er im Master .


So war es, als ich Benutzereinstellungen in IO.inspect/2 benötigte und in einigen Fällen. Was würde passieren, wenn ich in Java darauf stoße? - Es ist beängstigend, sich das vorzustellen.




Hab ein schönes Makro!

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


All Articles