Elixir-Metaprogrammierungsgrundlagen

Metaprogrammierung ist eine mächtige, aber ziemlich komplexe Technik, dh ein Programm kann sich zur Laufzeit selbst analysieren oder sogar modifizieren. Viele moderne Sprachen unterstützen diese Funktion, und Elixir macht da keine Ausnahme. 

Mit der Metaprogrammierung können Sie neue komplexe Makros erstellen, die Codeausführung dynamisch definieren und verzögern, wodurch Sie präziseren und leistungsfähigeren Code schreiben können. Dies ist in der Tat ein fortgeschrittenes Thema, aber nach dem Lesen dieses Artikels erhalten Sie hoffentlich ein grundlegendes Verständnis dafür, wie Sie mit der Metaprogrammierung in Elixir beginnen.

In diesem Artikel erfahren Sie:

  • Was der abstrakte Syntaxbaum ist und wie Elixir-Code unter der Haube dargestellt wird.
  • Was zum Zitat und unquote Funktionen sind.
  • Was sind Makros und wie man mit ihnen arbeitet.
  • Werte mit Binding eingeben.
  • Warum Makros hygienisch sind.

Bevor ich anfange, möchte ich Ihnen jedoch einen kleinen Rat geben. Erinnern Sie sich, dass der Onkel von Spider Man sagte: "Mit großer Kraft kommt große Verantwortung" Dies kann auch auf die Metaprogrammierung angewendet werden, da dies eine sehr leistungsfähige Funktion ist, mit der Sie Code nach Ihrem Willen drehen und biegen können. 

Trotzdem dürfen Sie es nicht missbrauchen, und Sie sollten sich an einfachere Lösungen halten, wenn es vernünftig und möglich ist. Zu viel Metaprogrammierung kann dazu führen, dass Ihr Code viel schwieriger zu verstehen und zu pflegen ist. Seien Sie also vorsichtig.

Abstrakter Syntaxbaum und Zitat

Als erstes müssen wir verstehen, wie unser Elixir-Code tatsächlich dargestellt wird. Diese Darstellungen werden häufig als Abstrakte Syntax-Bäume (AST) bezeichnet. Der offizielle Elixir-Leitfaden empfiehlt jedoch, sie einfach anzurufen Zitierte Ausdrücke

Es scheint, dass Ausdrücke in Form von Tupeln mit drei Elementen vorliegen. Aber wie können wir das beweisen? Nun, es gibt eine Funktion namens Zitat das gibt eine Darstellung für einen bestimmten Code zurück. Im Grunde wird der Code zu einer unbewertete Form. Zum Beispiel:

quote do 1 + 2 end # => : +, [Kontext: Elixier, Import: Kernel], [1, 2]

Also, was ist hier los? Das Tupel kehrte vom zurück Zitat Funktion hat immer die folgenden drei Elemente:

  1. Atom oder ein anderes Tupel mit derselben Darstellung. In diesem Fall handelt es sich um ein Atom :+, Das heißt, wir führen Addition durch. Diese Form von Schreibvorgängen sollte übrigens bekannt sein, wenn Sie aus der Ruby-Welt stammen.
  2. Keyword-Liste mit Metadaten In diesem Beispiel sehen wir das Kernel Das Modul wurde für uns automatisch importiert.
  3. Liste der Argumente oder eines Atoms. In diesem Fall ist dies eine Liste mit den Argumenten 1 und 2.

Die Darstellung kann natürlich viel komplexer sein:

quote do Enum.each ([1,2,3], & (IO.puts (& 1))) Ende # => :., [], [: __ Aliase__, [Alias: false], [: Enum ],: each], [], # [[1, 2, 3], # : &, [], # [.., [], [: __ Aliase__, [Alias: false], [: IO],: put], [], # [: &, [], [1]]]

Andererseits geben einige Literale sich selbst zurück, wenn sie zitiert werden, insbesondere:

  • Atome
  • ganze Zahlen
  • schwimmt
  • Listen
  • Streicher
  • Tupel (aber nur mit zwei Elementen!)

Im nächsten Beispiel können wir sehen, dass ein Atom dieses Atom zurückgibt:

Zitat do: hi end # =>: hi

Nun, da wir wissen, wie der Code unter der Haube dargestellt wird, fahren wir mit dem nächsten Abschnitt fort und sehen, was Makros sind und warum zitierte Ausdrücke wichtig sind.

Makros

Makros sind Sonderformen wie Funktionen, jedoch solche, die zitierten Code zurückgeben. Dieser Code wird dann in die Anwendung eingefügt und dessen Ausführung verzögert. Interessant ist, dass Makros auch die an sie übergebenen Parameter nicht auswerten, sondern auch als zitierte Ausdrücke dargestellt werden. Mit Makros können Sie benutzerdefinierte, komplexe Funktionen erstellen, die im gesamten Projekt verwendet werden. 

Beachten Sie jedoch, dass Makros komplexer sind als reguläre Funktionen und der offizielle Leitfaden besagt, dass sie nur als letztes Mittel verwendet werden sollten. Mit anderen Worten: Wenn Sie eine Funktion verwenden können, erstellen Sie kein Makro, da auf diese Weise Ihr Code unnötig komplex und effektiv schwieriger zu verwalten ist. Trotzdem haben Makros ihre Anwendungsfälle, also schauen wir uns an, wie man einen erstellt.

Alles beginnt mit dem defmacro Aufruf (was eigentlich ein Makro selbst ist):

defmodule MyLib defmacro test (arg) do arg |> IO.inspect end end

Dieses Makro akzeptiert einfach ein Argument und druckt es aus.

Erwähnenswert ist auch, dass Makros ebenso wie Funktionen privat sein können. Private Makros können nur von dem Modul aus aufgerufen werden, in dem sie definiert wurden. Um ein solches Makro zu definieren, verwenden Sie defmacrop.

Nun erstellen wir ein separates Modul, das als Spielplatz verwendet wird:

defmodule Main erfordert MyLib def start! Führen Sie MyLib.test (1,2,3) end end Main.start aus!

Wenn Sie diesen Code ausführen, : , [zeile: 11], [1, 2, 3] wird ausgedruckt, was bedeutet, dass das Argument eine zitierte (nicht ausgewertete) Form hat. Bevor Sie fortfahren, lassen Sie mich jedoch eine kleine Notiz machen.

Benötigen

Warum in aller Welt haben wir zwei separate Module erstellt: eines für die Definition eines Makros und eines für die Ausführung des Beispielcodes. Es scheint, dass wir es so machen müssen, weil Makros vor der Ausführung des Programms verarbeitet werden. Wir müssen auch sicherstellen, dass das definierte Makro im Modul verfügbar ist. Dies geschieht mit Hilfe von benötigen. Diese Funktion stellt im Wesentlichen sicher, dass das angegebene Modul vor dem aktuellen Modul kompiliert wird.

Sie fragen sich vielleicht, warum können wir das Hauptmodul nicht loswerden? Versuchen wir es einmal so:

defmodule MyLib defmacro test (arg) do arg |> IO.inspect Ende Ende MyLib.test (1,2,3) # => ** (UndefinedFunctionError) Die Funktion MyLib.test / 1 ist nicht definiert oder privat. Es gibt jedoch ein Makro mit demselben Namen und derselben Eigenschaft. Stellen Sie sicher, dass Sie MyLib benötigen, wenn Sie dieses Makro # MyLib.test (1, 2, 3) # (elixir) lib / code.ex: 376: Code.require_file / 2 aufrufen möchten

Leider erhalten wir eine Fehlermeldung, dass der Funktionstest nicht gefunden werden kann, obwohl es ein Makro mit demselben Namen gibt. Dies geschieht, weil die MyLib Das Modul ist in demselben Bereich (und in derselben Datei) definiert, in dem wir es verwenden wollen. Es mag ein bisschen seltsam erscheinen, aber denken Sie zunächst daran, dass ein separates Modul erstellt werden sollte, um solche Situationen zu vermeiden.

Beachten Sie auch, dass Makros nicht global verwendet werden können: Zuerst müssen Sie das entsprechende Modul importieren oder benötigen.

Makros und Anführungszeichen

Wir wissen also, wie Elixir-Ausdrücke intern dargestellt werden und welche Makros… Was nun? Nun, wir können dieses Wissen nutzen und sehen, wie der zitierte Code ausgewertet werden kann.

Kommen wir zu unseren Makros zurück. Es ist wichtig zu wissen, dass die letzter Ausdruck Von jedem Makro wird erwartet, dass es sich um einen Code handelt, der in Anführungszeichen steht und automatisch ausgeführt wird, wenn das Makro aufgerufen wird. Wir können das Beispiel aus dem vorherigen Abschnitt durch Verschieben neu schreiben IO.inspect zum Main Modul: 

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

Schau was passiert? Das vom Makro zurückgegebene Tupel wird nicht in Anführungszeichen gesetzt, sondern ausgewertet! Sie können versuchen, zwei Ganzzahlen hinzuzufügen:

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

Wieder wurde der Code ausgeführt und 3 wurde zurückgegeben. Wir können sogar versuchen, die Zitat Funktion direkt und die letzte Zeile wird noch ausgewertet:

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

Das arg wurde in Anführungszeichen gesetzt (man beachte übrigens, dass man sogar die Zeilennummer sehen kann, in der das Makro aufgerufen wurde), aber der in Anführungszeichen angegebene Ausdruck mit dem Tupel 1,2,3 wurde für uns ausgewertet, da dies die letzte Zeile des Makros ist.

Wir könnten versucht sein, die Verwendung von arg in einem mathematischen Ausdruck:

 defmacro test (arg) zitiere do arg + 1 end end

Dies wird jedoch einen Fehler auslösen, der das sagt arg ist nicht vorhanden. Warum so Das ist weil arg wird wörtlich in den von uns angegebenen String eingefügt. Aber wir möchten stattdessen das bewerten arg, Fügen Sie das Ergebnis in die Zeichenfolge ein und führen Sie dann die Anführungszeichen aus. Dazu benötigen wir eine weitere Funktion namens unquote.

Den Code aufheben

unquote ist eine Funktion, die das Ergebnis der Code-Auswertung in den Code einfügt, der dann zitiert wird. Das hört sich vielleicht etwas bizarr an, aber in Wirklichkeit sind die Dinge ganz einfach. Lassen Sie uns das vorherige Codebeispiel optimieren:

 defmacro test (arg) zitiert unquote (arg) + 1 end end

Jetzt wird unser Programm zurückkehren 4, das ist genau was wir wollten! Was passiert ist, dass der Code an den übergeben wurde unquote Die Funktion wird nur ausgeführt, wenn der angegebene Code ausgeführt wird, nicht wenn er zum ersten Mal analysiert wird.

Sehen wir uns ein etwas komplexeres Beispiel an. Angenommen, wir möchten eine Funktion erstellen, die einen Ausdruck ausführt, wenn die angegebene Zeichenfolge ein Palindrom ist. Wir könnten so etwas schreiben:

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

Das _f Das Suffix hier bedeutet, dass dies eine Funktion ist, da wir später ein ähnliches Makro erstellen werden. Wenn Sie jedoch jetzt versuchen, diese Funktion auszuführen, wird der Text gedruckt, obwohl die Zeichenfolge kein Palindrom ist:

 def start do MyLib.if_palindrome_f? ("745", IO.puts ("yes")) # => "yes" enden

Die an die Funktion übergebenen Argumente werden ausgewertet, bevor die Funktion tatsächlich aufgerufen wird "Ja" String auf dem Bildschirm ausgedruckt. Dies ist in der Tat nicht das, was wir erreichen wollen, also versuchen wir es stattdessen mit einem Makro:

 defmacro if_palindrome? (str, expr) do zitieren, wenn (unquote (str) == String.reverse (unquote (str))) unquote (expr) end end # ... MyLib.if_palindrome? ("745", IO. setzt ("ja"))

Hier zitieren wir den Code, der das enthält ob Zustand und Verwendung unquote inside, um die Werte der Argumente auszuwerten, wenn das Makro tatsächlich aufgerufen wird. In diesem Beispiel wird nichts auf dem Bildschirm ausgegeben, was korrekt ist!

Werte mit Bindungen injizieren

Verwenden unquote ist nicht die einzige Möglichkeit, Code in einen zitierten Block einzufügen. Wir können auch eine Funktion namens verwenden Bindung. Eigentlich ist dies einfach eine Option, die an die übergeben wird Zitat Funktion, die eine Schlüsselwortliste mit allen Variablen akzeptiert, die nicht in Anführungszeichen gesetzt werden sollen nur einmal.

Um das Binden durchzuführen, übergeben Sie bind_quoted zum Zitat Funktion wie folgt:

quote bind_quoted: [expr: expr] do ende

Dies kann nützlich sein, wenn der Ausdruck, der an mehreren Stellen verwendet wird, nur einmal ausgewertet werden soll. Wie dieses Beispiel zeigt, können wir ein einfaches Makro erstellen, das eine Zeichenfolge zweimal mit einer Verzögerung von zwei Sekunden ausgibt:

defmodule MyLib defmacro test (arg) zitiert bind_quoted: [arg: arg] do arg |> IO.inspect Process.sleep 2000 arg |> IO.inspect end end end

Wenn Sie jetzt die Systemzeit übergeben, haben die beiden Zeilen dasselbe Ergebnis:

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

Dies ist bei nicht der Fall unquote, da das Argument zweimal mit einer kleinen Verzögerung ausgewertet wird, sind die Ergebnisse nicht gleich:

 defmacro test (arg) zitieren 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

Konvertieren von zitiertem Code

Manchmal möchten Sie vielleicht wissen, wie Ihr zitierter Code tatsächlich aussieht, um ihn beispielsweise zu debuggen. Dies kann mit der to_string Funktion:

 defmacro if_palindrome? (str, expr) do quoted = quote do, wenn (unquote (str) == String.reverse (unquote (str))) unquote (expr) end end zitiert |> Macro.to_string |> IO.inspect zitiert Ende

Die gedruckte Zeichenfolge lautet:

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

Wir können das Gegebene sehen str Das Argument wurde ausgewertet und das Ergebnis wurde direkt in den Code eingefügt. \ n hier bedeutet "neue Linie".

 Wir können den zitierten Code auch mit erweitern expand_once und erweitern:

 def start do quoted = quote do MyLib.if_palindrome? ("745", IO.puts ("yes")) endet mit Anführungszeichen |> Macro.expand_once (__ ENV__) |> IO.inspect end

Was produziert:

: if, [context: MyLib, import: Kernel], [: ==, [context: MyLib, import: Kernel], ["745", :., [], [: __ Aliase__, [Alias : false, counter: -576460752303423103], [: String],: reverse], [], ["745"]], [do: :., [], [: __ Aliase__, [Alias : false, counter: -576460752303423103], [: IO],: put], [], ["yes"]]]]

Diese zitierte Darstellung kann natürlich wieder in eine Zeichenfolge umgewandelt werden:

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

Wir erhalten das gleiche Ergebnis wie zuvor:

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

Das erweitern Funktion ist komplexer, da versucht wird, jedes Makro in einem bestimmten Code zu erweitern:

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

Das Ergebnis wird sein:

"case (" 745 "== String.reverse (" 745 ")) do \ nx, wenn x in [false, nil] -> \ n nil \ n _ -> \ n IO.puts (\" ja \ ") \ nend"

Wir sehen diese Ausgabe da ob ist eigentlich ein Makro selbst, das sich auf die Fall Aussage, so wird es auch erweitert.

In diesen Beispielen, __ENV__ ist ein spezielles Formular, das Umgebungsinformationen wie aktuelles Modul, Datei, Zeile, Variable im aktuellen Bereich und Importe zurückgibt.

Makros sind hygienisch

Sie haben vielleicht gehört, dass Makros tatsächlich sind hygienisch. Dies bedeutet, dass sie keine Variablen außerhalb ihres Gültigkeitsbereichs überschreiben. Um dies zu beweisen, fügen Sie eine Beispielvariable hinzu, ändern Sie den Wert an verschiedenen Stellen und geben Sie ihn dann aus:

 defmacro if_palindrome? (str, expr) do other_var = "if_palindrome?" quoted = quote do other_var = "quoted" wenn (unquote (str) == String.reverse (unquote (str))) nicht quot (expr) end other_var |> IO.inspect end other_var |> IO.inspect quoted end #… def start do other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect endet

So other_var wurde einen Wert innerhalb der angegeben Start! Funktion, im Makro und im Zitat. Sie sehen die folgende Ausgabe:

"if_palindrome?" "Anführungszeichen" "Start!"

Das bedeutet, dass unsere Variablen unabhängig sind, und wir führen keine Konflikte ein, indem wir überall denselben Namen verwenden (obwohl es natürlich besser wäre, sich von einem solchen Ansatz fernzuhalten).. 

Wenn Sie die äußere Variable wirklich innerhalb eines Makros ändern müssen, können Sie sie verwenden var! so was:

 defmacro if_palindrome? (str, expr) do quoted = quote do var! (other_var) = "quoted" if (unquote (str) == String.reverse (unquote (str))) nicht zitieren (expr) Ende beendet … Def start! do other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect # => "quoted" endet

Durch die Nutzung var!, Wir sagen effektiv, dass die gegebene Variable nicht hygienisiert werden sollte. Seien Sie jedoch vorsichtig, wenn Sie diesen Ansatz verwenden, da Sie sonst den Überblick verlieren, was wo überschrieben wird.

Fazit

In diesem Artikel haben wir die Grundlagen der Metaprogrammierung in der Elixiersprache erörtert. Wir haben die Verwendung von abgedeckt Zitat, unquote, Makros und Bindungen, während Sie einige Beispiele und Anwendungsfälle sehen. An diesem Punkt können Sie dieses Wissen in der Praxis anwenden und prägnantere und leistungsfähigere Programme erstellen. Denken Sie jedoch daran, dass es in der Regel besser ist, verständlichen Code zu haben als prägnanter Code. Überbeanspruchen Sie also nicht die Metaprogrammierung in Ihren Projekten.

Wenn Sie mehr über die beschriebenen Funktionen erfahren möchten, lesen Sie bitte den offiziellen Leitfaden "Erste Schritte" zu Makros, Zitat und unquote. Ich hoffe wirklich, dass dieser Artikel Ihnen eine schöne Einführung in die Metaprogrammierung in Elixir gab, die auf den ersten Blick recht komplex erscheinen kann. Scheuen Sie sich jedenfalls nicht, mit diesen neuen Tools zu experimentieren!

Ich danke Ihnen, dass Sie bei mir geblieben sind, und bis bald.