Elixir Metaprogramming Basics

Metaprogrammering is een krachtige, maar toch vrij complexe techniek, wat betekent dat een programma zichzelf tijdens runtime kan analyseren of zelfs kan aanpassen. Veel moderne talen ondersteunen deze functie, en Elixir is geen uitzondering. 

Met metaprogrammering kunt u nieuwe complexe macro's maken, dynamisch code definiëren en uitstellen, waardoor u meer beknopte en krachtige code kunt schrijven. Dit is inderdaad een geavanceerd onderwerp, maar hopelijk krijg je na het lezen van dit artikel een basisbegrip over hoe je aan de slag kunt met metaprogrammering in Elixir.

In dit artikel leer je:

  • Wat de abstracte syntaxisboom is en hoe Elixir-code wordt weergegeven onder de motorkap.
  • Wat de citaat en unquote functies zijn.
  • Welke macro's zijn en hoe ermee te werken.
  • Hoe waarden te injecteren met binding.
  • Waarom macro's hygiënisch zijn.

Laat ik u echter eerst een klein advies geven voordat u begint. Onthoud dat de oom van Spider Man zei: "Met grote macht komt grote verantwoordelijkheid"? Dit kan ook worden toegepast op metaprogrammering, omdat dit een zeer krachtige functie is waarmee je code naar je hand kunt draaien en buigen. 

Maar je moet het niet misbruiken, en je moet je houden aan eenvoudigere oplossingen als het gezond en mogelijk is. Te veel metaprogrammering kan uw code veel moeilijker te begrijpen en te onderhouden maken, dus wees voorzichtig.

Samenvatting Syntaxisboom en Citaat

Het eerste dat we moeten begrijpen, is hoe onze Elixir-code daadwerkelijk wordt weergegeven. Deze representaties worden vaak abstracte syntaxisbomen (AST) genoemd, maar de officiële Elixir-gids beveelt aan ze eenvoudig te noemen geciteerde uitdrukkingen

Het lijkt erop dat uitdrukkingen komen in de vorm van tuples met drie elementen. Maar hoe kunnen we dat bewijzen? Welnu, er is een functie genaamd citaat die een weergave retourneert voor een gegeven code. Kortom, het maakt de code veranderen in een niet-geëvalueerde vorm. Bijvoorbeeld:

quote do 1 + 2 end # => : +, [context: Elixir, import: Kernel], [1, 2]

Dus wat is hier aan de hand? Het tuple geretourneerd door de citaat functie heeft altijd de volgende drie elementen:

  1. Atoom of een andere tupel met dezelfde representatie. In dit geval is het een atoom :+, wat betekent dat we een toevoeging uitvoeren. Trouwens, deze vorm van schrijfbewerkingen moet bekend zijn als je uit de Ruby-wereld komt.
  2. Zoekwoordenlijst met metadata. In dit voorbeeld zien we dat de pit module is automatisch voor ons geïmporteerd.
  3. Lijst met argumenten of een atoom. In dit geval is dit een lijst met de argumenten 1 en 2.

De weergave kan natuurlijk veel complexer zijn:

quote do Enum.each ([1,2,3], & (IO.puts (& 1))) end # => :., [], [: __ aliases__, [alias: false], [: Enum ],: each], [], # [[1, 2, 3], # : &, [], # [:., [], [: __ aliases__, [alias: false], [: IO],: puts], [], # [: &, [], [1]]]]

Aan de andere kant keren sommige letterlijke woorden terug wanneer ze worden geciteerd, in het bijzonder:

  • atomen
  • integers
  • drijvers
  • lijsten
  • strings
  • tuples (maar alleen met twee elementen!)

In het volgende voorbeeld kunnen we zien dat het citeren van een atoom dit atoom teruggeeft:

quote do: hi end # =>: hoi

Nu we weten hoe de code onder de motorkap wordt weergegeven, gaan we verder met de volgende sectie en zien we welke macro's zijn en waarom geciteerde uitdrukkingen belangrijk zijn.

macro's

Macro's zijn speciale vormen zoals functies, maar degenen die geretourneerde code retourneren. Deze code wordt vervolgens in de toepassing geplaatst en de uitvoering ervan wordt uitgesteld. Wat interessant is, is dat macro's ook de parameters die aan hen worden doorgegeven niet evalueren - ze worden ook weergegeven als geciteerde uitdrukkingen. Macro's kunnen worden gebruikt om aangepaste, complexe functies te maken die in uw project worden gebruikt. 

Houd er echter rekening mee dat macro's complexer zijn dan reguliere functies en de officiële gids stelt dat ze alleen als laatste redmiddel moeten worden gebruikt. Met andere woorden: als u een functie kunt gebruiken, maakt u geen macro, omdat uw code hierdoor onnodig complex en, moeilijker te onderhouden, wordt. Macro's hebben echter wel hun use-cases, dus laten we eens kijken hoe we er een kunnen maken.

Het begint allemaal met de defmacro call (wat eigenlijk een macro zelf is):

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

Deze macro accepteert eenvoudig een argument en drukt het uit.

Het is ook de moeite waard om te vermelden dat macro's privé kunnen zijn, net als functies. Privé-macro's kunnen alleen worden aangeroepen vanuit de module waarin ze zijn gedefinieerd. Gebruik om een ​​dergelijke macro te definiëren defmacrop.

Laten we nu een aparte module maken die als onze speelplaats zal worden gebruikt:

defmodule De hoofdfunctie vereist dat MyLib def start! do MyLib.test (1,2,3) end-end Main.start!

Wanneer u deze code uitvoert, : , [regel: 11], [1, 2, 3] wordt afgedrukt, wat inderdaad betekent dat het argument een geciteerde (ongeëvalueerde) vorm heeft. Alvorens verder te gaan, wil ik echter een kleine aantekening maken.

Vereisen

Waarom hebben we in de wereld twee afzonderlijke modules gemaakt: een om een ​​macro te definiëren en een andere om de voorbeeldcode uit te voeren? Het lijkt erop dat we het op deze manier moeten doen, omdat macro's worden verwerkt voordat het programma wordt uitgevoerd. We moeten er ook voor zorgen dat de gedefinieerde macro beschikbaar is in de module, en dit gebeurt met de hulp van vereisen. Deze functie zorgt er in principe voor dat de gegeven module vóór de huidige wordt gecompileerd.

Je zou je kunnen afvragen, waarom kunnen we de hoofdmodule niet verwijderen? Laten we proberen dit te doen:

defmodule MyLib do defmacro test (arg) do arg |> IO.inzicht end end MyLib.test (1,2,3) # => ** (UndefinedFunctionError) -functie MyLib.test / 1 is ongedefinieerd of privé. Er is echter een macro met dezelfde naam en arity. Zorg ervoor dat u MyLib nodig hebt als u van plan bent om deze macro aan te roepen # MyLib.test (1, 2, 3) # (elixer) lib / code.ex: 376: Code.require_file / 2

Helaas krijgen we een foutmelding dat de functietest niet kan worden gevonden, hoewel er een macro met dezelfde naam is. Dit gebeurt omdat het MyLib module wordt gedefinieerd in hetzelfde bereik (en hetzelfde bestand) waar we het proberen te gebruiken. Het lijkt misschien een beetje raar, maar onthoud voor nu dat er een aparte module moet worden gemaakt om dergelijke situaties te voorkomen.

Merk ook op dat macro's niet wereldwijd kunnen worden gebruikt: eerst moet u de bijbehorende module importeren of vereisen.

Macro's en geciteerde uitdrukkingen

Dus we weten hoe Elixir-uitdrukkingen intern worden weergegeven en welke macro's zijn ... Wat nu? Welnu, nu kunnen we deze kennis gebruiken en zien hoe de geciteerde code kan worden geëvalueerd.

Laten we terugkeren naar onze macro's. Het is belangrijk om te weten dat de laatste uitdrukking Van elke macro wordt verwacht dat deze een quotecode is die automatisch wordt uitgevoerd en geretourneerd wanneer de macro wordt aangeroepen. We kunnen het voorbeeld uit het vorige gedeelte herschrijven door te verplaatsen IO.inspect naar de Hoofd module: 

defmodule MyLib doe defmacro-test (arg) doe arg end-end defmodule Main do require MyLib def start! do MyLib.test (1,2,3) |> IO.inzicht end-end Main.start! # => 1, 2, 3

Kijken wat er gebeurt? Het tuple dat door de macro wordt geretourneerd, wordt niet geciteerd maar geëvalueerd! U kunt proberen twee gehele getallen toe te voegen:

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

Wederom is de code uitgevoerd, en 3 was teruggekeerd. We kunnen zelfs proberen de citaat functie direct, en de laatste regel zal nog steeds worden geëvalueerd:

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

De arg werd geciteerd (merk overigens op dat we zelfs het regelnummer kunnen zien waar de macro werd aangeroepen), maar de geciteerde uitdrukking met het tuple 1,2,3 werd voor ons geëvalueerd omdat dit de laatste regel van de macro is.

We komen misschien in de verleiding om het te gebruiken arg in een wiskundige uitdrukking:

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

Maar dit zal een fout oproepen die dat zegt arg bestaat niet. Waarom? Dit is zo omdat arg wordt letterlijk ingevoegd in de string die we citeren. Maar wat we in plaats daarvan willen doen, is het evalueren van de arg, plaats het resultaat in de tekenreeks en voer vervolgens de aanhalingstekens uit. Om dit te doen, zullen we nog een andere functie nodig hebben unquote.

Unquoting van de code

unquote is een functie die het resultaat van de codewaardering in de code injecteert die vervolgens wordt geciteerd. Dit klinkt misschien een beetje bizar, maar in werkelijkheid zijn de dingen vrij eenvoudig. Laten we het vorige codevoorbeeld aanpassen:

 defmacro-test (arg) do quote do unquote (arg) + 1 end end

Nu zal ons programma terugkeren 4, dat is precies wat we wilden! Wat er gebeurt, is dat de code is doorgegeven aan de unquote De functie wordt alleen uitgevoerd wanneer de gequoteerde code wordt uitgevoerd, niet wanneer deze in eerste instantie wordt geparseerd.

Laten we een iets complexer voorbeeld bekijken. Stel dat we een functie willen creëren die wat uitdrukking geeft als de gegeven reeks een palindroom is. We kunnen zoiets als dit schrijven:

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

De _F Achtervoegsel betekent hier dat dit een functie is, omdat we later een vergelijkbare macro zullen maken. Als we echter deze functie nu proberen uit te voeren, wordt de tekst afgedrukt, ook al is de tekenreeks geen palindroom:

 begin maar! do MyLib.if_palindrome_f? ("745", IO.puts ("yes")) # => "yes" end

De argumenten die aan de functie worden doorgegeven, worden geëvalueerd voordat de functie daadwerkelijk wordt aangeroepen, dus we zien de "Ja" string afgedrukt op het scherm. Dit is inderdaad niet wat we willen bereiken, dus laten we proberen in plaats daarvan een macro te gebruiken:

 defmacro if_palindrome? (str, expr) citaat doen als (unquote (str) == String.reverse (unquote (str))) unquote (expr) end end end # ... MyLib.if_palindrome? ("745", IO. puts ( "ja"))

Hier citeren we de code met de als conditie en gebruik unquote binnen om de waarden van de argumenten te evalueren wanneer de macro daadwerkelijk wordt aangeroepen. In dit voorbeeld wordt niets op het scherm afgedrukt, wat correct is!

Waarden injecteren met bindingen

Gebruik makend van unquote is niet de enige manier om code in een geciteerd blok te injecteren. We kunnen ook een functie gebruiken genaamd verbindend. Eigenlijk is dit gewoon een optie die wordt doorgegeven aan de citaat functie die een lijst met zoekwoorden accepteert met alle variabelen die niet moeten worden vermeld slechts één keer.

Om te binden, pas bind_quoted naar de citaat functioneer als volgt:

quote bind_quoted: [expr: expr] do end

Dit kan van pas komen als u wilt dat de uitdrukking die op meerdere plaatsen wordt gebruikt slechts eenmaal wordt geëvalueerd. Zoals in dit voorbeeld is aangetoond, kunnen we een eenvoudige macro maken die tweemaal een reeks uitvoert met een vertraging van twee seconden:

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

Als u het nu noemt door de systeemtijd door te geven, hebben de twee regels hetzelfde resultaat:

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

Dit is niet het geval met unquote, omdat het argument twee keer met een kleine vertraging zal worden geëvalueerd, dus de resultaten zijn niet hetzelfde:

 defmacro test (arg) do quote do unquote (arg) | IO.inspect Process.sleep (2000) unquote (arg) |> IO.inspect end end # ... def start! do: os.system_time |> MyLib.test # => 1547457934011392 # => 1547457936059392 einde

Geciteerde code converteren

Soms wilt u wellicht weten hoe uw quotering er precies uitziet om het te debuggen. Dit kan gedaan worden door de to_string functie:

 defmacro if_palindrome? (str, expr) geciteerd = quote do if (unquote (str) == String.reverse (unquote (str))) not unquote (expr) end end quoted |> Macro.to_string |> IO.inspect geciteerd einde

De afgedrukte reeks zal zijn:

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

We kunnen zien dat het gegeven str argument is geëvalueerd en het resultaat is direct in de code ingevoegd. \ n hier betekent "nieuwe regel".

 We kunnen de geciteerde code ook uitbreiden met expand_once en uitbreiden:

 begin maar! geciteerd = quote do MyLib.if_palindrome? ("745", IO.puts ("yes")) end quoted |> Macro.expand_once (__ ENV__) |> IO.inzicht einde

Die produceert:

: if, [context: MyLib, import: Kernel], [: ==, [context: MyLib, import: Kernel], ["745", :, [], [: __ aliases__, [alias : false, counter: -576460752303423103], [: String],: reverse], [], ["745"]], [do: :., [], [: __ aliases__, [alias : false, counter: -576460752303423103], [: IO],: puts], [], ["yes"]]]

Natuurlijk kan deze geciteerde weergave worden teruggedraaid naar een tekenreeks:

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

We krijgen hetzelfde resultaat als voorheen:

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

De uitbreiden functie is complexer omdat het probeert elke macro in een gegeven code uit te breiden:

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

Het resultaat zal zijn:

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

We zien deze uitvoer omdat als is eigenlijk een macro zelf die vertrouwt op de geval verklaring, dus het wordt ook uitgebreid.

In deze voorbeelden, __ENV__ is een speciaal formulier dat milieu-informatie retourneert, zoals de huidige module, bestand, regel, variabele in het huidige bereik en import.

Macro's zijn hygiënisch

Je hebt misschien gehoord dat macro's eigenlijk zijn hygiënisch. Wat dit betekent is dat ze geen variabelen overschrijven die buiten hun bereik vallen. Om het te bewijzen, laten we een voorbeeldvariabele toevoegen, proberen de waarde ervan op verschillende plaatsen te wijzigen en deze vervolgens uitvoeren:

 defmacro if_palindrome? (str, expr) do other_var = "if_palindrome?" quoted = quote do other_var = "quoted" if (unquote (str) == String.reverse (unquote (str))) do unquote (expr) end other_var |> IO.inzicht end other_var |> IO.inspect geciteerd einde # ... begin maar! do other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect end

Zo other_var kreeg een waarde binnen de begin! functie, in de macro en in de citaat. U ziet de volgende uitvoer:

"If_palindrome?" "geciteerd" "start!"

Dit betekent dat onze variabelen onafhankelijk zijn en we introduceren geen conflicten door overal dezelfde naam te gebruiken (hoewel het natuurlijk beter is om uit de buurt van een dergelijke benadering te blijven). 

Als u de externe variabele in een macro echt wilt wijzigen, kunt u deze gebruiken var! zoals dit:

 defmacro if_palindrome? (str, expr) geciteerd = quote do var! (other_var) = "quoted" if (unquote (str) == String.reverse (unquote (str))) notquote (expr) end end quoted end # ... def begin! do other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect # => "geciteerd" einde

Door het gebruiken van var!, we zeggen effectief dat de gegeven variabele niet moet worden gehygiëniseerd. Wees echter zeer voorzichtig met het gebruik van deze methode, omdat u misschien uit het oog verliest wat er wordt overschreven.

Conclusie

In dit artikel hebben we de basisbeginselen van metaprogrammering besproken in de Elixir-taal. We hebben het gebruik van gedekt citaat, unquote, macro's en bindingen terwijl je enkele voorbeelden ziet en cases gebruikt. Op dit moment ben je klaar om deze kennis in de praktijk toe te passen en meer beknopte en krachtige programma's te maken. Bedenk echter dat het meestal beter is om begrijpelijke code te hebben dan beknopte code, dus gebruik niet te veel programmeren in uw projecten.

Als u meer wilt weten over de functies die ik heb beschreven, kunt u de officiële handleiding 'Aan de slag' over macro's, citaten en offertes lezen. Ik hoop echt dat dit artikel je een aardige inleiding gaf tot metaprogrammering in Elixir, dat in eerste instantie inderdaad behoorlijk complex kan lijken. Wees in elk geval niet bang om met deze nieuwe tools te experimenteren!

Ik dank u dat u bij mij bent gebleven en tot ziens.