Bazele de metaprogramare a elixirului

Metaprogramarea este o tehnică puternică, dar destul de complexă, ceea ce înseamnă că un program poate analiza sau chiar se poate modifica în timpul execuției. Multe limbi moderne acceptă această caracteristică, iar Elixirul nu face excepție. 

Cu metroprogramare, puteți crea noi macrocomenzi complexe, definiți dinamic și amâna execuția codului, ceea ce vă permite să scrieți un cod mai concis și mai puternic. Acesta este într-adevăr un subiect avansat, dar, sperăm, după ce ați citit acest articol veți obține o înțelegere de bază a modului de a începe cu metaprogramarea în Elixir.

În acest articol veți învăța:

  • Ce arbore de sintaxă abstractă este și cum este reprezentat codul Elixir sub capotă.
  • Ce citat și unquote funcțiile sunt.
  • Ce macrocomenzi sunt și cum să lucrați cu ele.
  • Cum să injectați valorile cu legarea.
  • De ce macrourile sunt igienice.

Înainte de a începe, totuși, permiteți-mi să vă dau o mică sfat. Amintiți-vă unchiul Spider Man a spus "Cu mare putere vine mare responsabilitate"? Acest lucru poate fi aplicat și metaprogramării deoarece aceasta este o caracteristică foarte puternică care vă permite să răsuciți și să îndoiți codul conform voinței dvs.. 

Totuși, nu trebuie să o abuzezi și trebuie să rămâi la soluții mai simple atunci când este sanatos și posibil. Prea multă metaprogramare poate face codul dvs. mult mai greu de înțeles și de întreținut, deci fiți atent în privința acestuia.

Sintaxă abstractă și Citat

Primul lucru pe care trebuie să-l înțelegem este modul în care este reprezentat codul nostru Elixir. Aceste reprezentări sunt adesea numite Copaci de sinteză abstractă (AST), dar ghidul oficial Elixir recomandă să le numiți pur și simplu expresii citate

Se pare că expresiile vin sub formă de tupluri cu trei elemente. Dar cum putem dovedi asta? Ei bine, există o funcție numită citat care returnează o reprezentare pentru un anumit cod dat. Practic, codul se transformă într-un formă neevaluată. De exemplu:

citează numărul 1 + 2 # => : +, [context: Elixir, import: Kernel], [1, 2]

Ce se întâmplă aici? Tuplul returnat de către citat Funcția are întotdeauna următoarele trei elemente:

  1. Atom sau o altă tuplă cu aceeași reprezentare. În acest caz, este un atom :+, adică noi performăm suplimentar. Apropo, această formă de operațiuni de scriere ar trebui să fie familiară dacă ați venit din lumea Ruby.
  2. Lista de cuvinte cheie cu metadate. În acest exemplu vedem că Nucleu modulul a fost importat automat pentru noi.
  3. Lista argumentelor sau a unui atom. În acest caz, aceasta este o listă cu argumente 1 și 2.

Reprezentarea poate fi mult mai complexă, desigur:

citează Enum.each ([1,2,3], & (IO.puts (& 1))) sfârșitul # => :., [], [: ]], [], # [: 1, 2, 3], # : &, [], # [:., [], alias: [: IO],: puts], [], # [: &, [], [1]]]

Pe de altă parte, unii literali se întorc atunci când sunt citați:

  • atomi
  • numere întregi
  • flotoare
  • liste
  • siruri de caractere
  • tuple (dar numai cu două elemente!)

În următorul exemplu, putem vedea că citarea unui atom returnează acest atom:

citează: hi end # =>: hi

Acum, când știm cum este reprezentat codul sub capotă, să mergem la următoarea secțiune și să vedem ce macrouri sunt și de ce expresiile citate sunt importante.

macrocomenzi

Macrorele sunt forme speciale, cum ar fi funcțiile, dar cele care returnează codul citat. Acest cod este apoi plasat în aplicație, iar executarea acestuia este amânată. Ceea ce este interesant este faptul că și macrocomenzile nu evaluează parametrii care le-au fost transmiși - ei sunt reprezentați și ca expresii citate. Macroanele pot fi folosite pentru a crea funcții personalizate și complexe folosite în întregul proiect. 

Rețineți, totuși, că macrocomenzile sunt mai complexe decât funcțiile obișnuite, iar ghidul oficial arată că acestea ar trebui folosite doar ca ultimă soluție. Cu alte cuvinte, dacă puteți utiliza o funcție, nu creați o macrocomandă, deoarece astfel codul dvs. devine inutil complex și, în mod eficient, mai greu de întreținut. Totuși, macrocomenzile au cazurile de utilizare a acestora, așa că să vedem cum să le creăm.

Totul începe cu defmacro apel (care este de fapt o macrocomanda în sine):

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

Această macrocomandă acceptă pur și simplu un argument și o imprimă.

De asemenea, merită menționat faptul că macrocomenzile pot fi private, la fel ca și funcțiile. Macro-urile private pot fi chemați numai din modul în care au fost definite. Pentru a defini o astfel de macrocomandă, utilizați defmacrop.

Acum, să creăm un modul separat care va fi folosit ca teren de joacă:

Defmodule principale necesită MyLib def start! faceți MyLib.test (1,2,3) end end Main.start!

Când executați acest cod, : , [line: 11], [1, 2, 3] va fi tipărită, ceea ce înseamnă într-adevăr că argumentul are o formă cotată (neevaluată). Înainte de a continua, totuși, permiteți-mi să fac o notă mică.

necesita

De ce în lume am creat două module separate: una pentru a defini o macrocomandă și una pentru a executa exemplul de cod? Se pare că trebuie să facem acest lucru, deoarece macrocomenzile sunt procesate înainte ca programul să fie executat. De asemenea, trebuie să ne asigurăm că macroul definit este disponibil în modul, iar acest lucru se face cu ajutorul necesita. Această funcție, în principiu, asigură faptul că modulul dat este compilat înainte de cel actual.

S-ar putea să întrebați de ce nu putem să scăpăm de modulul principal? Să încercăm să facem acest lucru:

deflodule MyLib defmacro test (arg) do arg || IO.inspect end end MyLib.test (1,2,3) # => ** (UndefinedFunctionError) funcția MyLib.test / 1 este nedefinită sau privată. Cu toate acestea, există o macrocomandă cu același nume și același nume. Asigurați-vă că cereți MyLib dacă intenționați să invocați această macrocomandă # MyLib.test (1, 2, 3) # (elixir) lib / code.ex: 376: Code.require_file / 2

Din păcate, primim o eroare spunând că testul funcțional nu poate fi găsit, deși există o macro cu același nume. Acest lucru se întâmplă pentru că MyLib modulul este definit în același domeniu (și același fișier) în cazul în care încercăm să îl folosim. Poate părea un pic ciudat, dar pentru moment doar amintiți-vă că trebuie creat un modul separat pentru a evita astfel de situații.

De asemenea, rețineți că macrocomenzile nu pot fi utilizate la nivel global: mai întâi trebuie să importați sau să solicitați modulul corespunzător.

Macroanele și expresiile citate

Deci știm cum expresiile Elixir sunt reprezentate intern și ce macrouri sunt ... Acum ce? Ei bine, acum putem folosi aceste cunoștințe și să vedem cum poate fi evaluat codul citat.

Să ne întoarcem la macrocomenzile noastre. Este important să știți că ultima expresie din orice macro este de așteptat să fie un cod citat, care va fi executat și returnat automat când macro-ul este chemat. Putem rescrie exemplul din secțiunea anterioară prin mișcare IO.inspect la Principal modul: 

defmodule MyLib face defmacro test (arg) face arg end end defmodule Principala necesita MyLib def start! faceți MyLib.test (1,2,3) |> IO.inspect sfârșitul final Main.start! # => 1, 2, 3

Vezi ce se intampla? Tuplul returnat de macrocomandă nu este cotat, ci evaluat! Puteți încerca să adăugați două numere întregi:

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

Încă o dată, codul a fost executat și 3 a fost returnat. Putem încerca chiar să folosim citat funcționează direct, iar ultima linie va fi încă evaluată:

defmodule MyLib face 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]

arg a fost citat (notează, de altfel, că putem vedea chiar și numărul liniei unde a fost apelată macrocomanda), dar expresia citată cu tupla 1,2,3 a fost evaluată pentru noi deoarece aceasta este ultima linie a macrocomenzii.

Am putea fi tentați să încercăm să folosim arg într-o expresie matematică:

 testul defmacro (arg) face quote do arg + 1 sfârșitul final

Dar acest lucru va ridica o eroare spunând asta arg nu exista. De ce? Asta pentru ca arg este literalmente inserat în șirul pe care îl cităm. Dar ceea ce ne-ar place să facem este evaluarea arg, introduceți rezultatul în șir, apoi efectuați citarea. Pentru a face acest lucru, vom avea nevoie de o altă funcție numită unquote.

Necotind codul

unquote este o funcție care injectează rezultatul evaluării codului în interiorul codului care va fi apoi citat. Acest lucru poate părea un pic bizar, dar în realitate lucrurile sunt destul de simple. Să modificăm exemplul codului anterior:

 testul defmacro (arg) face quote do unquote (arg) + 1 sfârșitul final

Acum programul nostru se va întoarce 4, care este exact ceea ce ne-am dorit! Ce se întâmplă este faptul că codul a trecut la unquote funcția este executată numai atunci când codul citat este executat, nu când este inițial analizat.

Să vedem un exemplu puțin mai complex. Să presupunem că am dori să creăm o funcție care rulează o expresie dacă șirul dat este un palindrom. Am putea scrie ceva de genul:

 def if_palindrome_f? (str, expr) face dacă str == String.reverse (str), do: expr end

_F sufix aici înseamnă că aceasta este o funcție, deoarece mai târziu vom crea o macrocomandă similară. Cu toate acestea, dacă încercăm să executăm această funcție acum, textul va fi tipărit chiar dacă șirul nu este un palindrom:

 începeți! do MyLib.if_palindrome_f? ("745", IO.puts ("da")) # => sfârșitul "da"

Argumentele transmise funcției sunt evaluate înainte ca funcția să fie apelată, deci vedem "da" șir imprimat pe ecran. Acest lucru nu este într-adevăr ceea ce vrem să realizăm, așa că să încercăm să folosim o macrocomandă:

 dacă nu (quote) () () () () () () () ( "da put"))

Aici menționăm codul care conține dacă condiție și utilizare unquote în interiorul pentru a evalua valorile argumentelor când macro-ul este de fapt apelat. În acest exemplu, nimic nu va fi imprimat pe ecran, ceea ce este corect!

Injectarea de valori cu legături

Utilizarea unquote nu este singura modalitate de a injecta codul într-un bloc citat. De asemenea, putem utiliza o funcție numită legare. De fapt, aceasta este pur și simplu o opțiune transmisă citat care acceptă o listă de cuvinte cheie cu toate variabilele care ar trebui să fie necotate doar o data.

Pentru a efectua legarea, treceți bind_quoted la citat funcționează astfel:

citează bind_quoted: [expr: expr] termină

Acest lucru poate fi util atunci când doriți ca expresia folosită în mai multe locuri să fie evaluată o singură dată. Așa cum se demonstrează prin acest exemplu, putem crea o macrocomandă simplă care emite un șir de două ori cu o întârziere de două secunde:

defmodule MyLib face defmacro test (arg) citează bind_quoted: [arg: arg] face arg |> IO.inspect Process.sleep 2000 arg |> IO.inspect capăt sfârșit sfârșit

Acum, dacă o numiți prin trecerea timpului de sistem, cele două linii vor avea același rezultat:

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

Nu este cazul unquote, deoarece argumentul va fi evaluat de două ori cu o întârziere mică, deci rezultatele nu sunt aceleași:

 defacro test (arg) face quote do unquote (arg) |> IO.inspect Process.sleep (2000) unquote (arg) |> IO.inspect capăt final # ... def start! do: os.system_time |> MyLib.test # => 1547457934011392 # => 1547457936059392 Sfârșit

Convertirea codului cotat

Uneori, poate doriți să înțelegeți cum arată codul dvs. citat, de exemplu, pentru ao depana. Acest lucru se poate face folosind to_string funcţie:

 (un) () () () () () () () () Sfârșit

Șirul imprimat va fi:

"dacă \" 745 \ "== String.reverse (\" 745 \ ")) nu \ n IO.puts (\" da \ ") \ nend"

Putem vedea acel dat str argumentul a fost evaluat, iar rezultatul a fost introdus chiar în cod. \ n aici înseamnă "linie nouă".

 De asemenea, putem extinde codul citat utilizând expand_once și extinde:

 începeți! quote = quote Do MyLib.if_palindrome? ("745", IO.puts ("da")) sfarsit citat |> Macro.expand_once (__ ENV__) |> IO.inspect end

Care produce:

:: context: MyLib, import: kernel], [: ==, context: MyLib, import: Kernel], [745], : : false, contra: -576460752303423103], [: String],: inversa], [], ["745"]], [:: alias: : false, contra: -576460752303423103], [: IO],: puts], [], ["da"]]]]

Desigur, această reprezentare citată poate fi întoarsă la un șir:

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

Vom obține același rezultat ca înainte:

"dacă \" 745 \ "== String.reverse (\" 745 \ ")) nu \ n IO.puts (\" da \ ") \ nend"

extinde funcția este mai complexă, deoarece încearcă să extindă fiecare macro într-un cod dat:

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

Rezultatul va fi:

"caz \ (\" 745 \ "== String.reverse (\" 745 \ ")) \ nx atunci când x în [false, nil] da \ ") \ Nend"

Vedem această ieșire deoarece dacă este de fapt o macrocomandă care se bazează pe caz declarație, astfel încât să se extindă și ea.

În aceste exemple, __ENV__ este un formular special care returnează informații despre mediu cum ar fi modulul curent, fișierul, linia, variabila în domeniul de aplicare actual și importurile.

Macroanele sunt igienice

S-ar putea să fi auzit că macrocomenzile sunt de fapt igienic. Ceea ce înseamnă că ele nu suprascriu nici o variabilă în afara domeniului lor de aplicare. Pentru a dovedi aceasta, să adăugăm o variabilă de mostră, să încercăm să schimbăm valoarea în diferite locuri și apoi să o emităm:

 defmacro if_palindrome? (str, expr) face alt_var = "if_palindrome?" quote = quote alt_var = "citat" daca (unquote (str) == String.reverse (unquote (str)) do unquote (expr) end other_var |> IO.inspect end other_var | începeți! do other_var = "începe!" MyLib.if_palindromă ("745", IO.puts ("da")) other_var |> IO.inspect sfârșit

Asa de other_var a primit o valoare în interiorul start! funcția, în interiorul macro și în interior citat. Veți vedea următoarea ieșire:

"If_palindrome?" "citat" "începe!"

Aceasta înseamnă că variabilele noastre sunt independente și nu introducem conflicte prin utilizarea aceluiași nume peste tot (deși, bineînțeles, ar fi mai bine să nu stați la o asemenea abordare). 

Dacă într-adevăr aveți nevoie să modificați variabila exterioară dintr-o macrocomandă, puteți utiliza var! asa:

 (unicat) () () () () () () () () ... începeți! do other_var = "începe!" MyLib.if_palindromă ("745", IO.puts ("da")) other_var |> IO.inspect # => "quoted" end

Prin utilizarea var!, spunem în mod eficient că variabila dată nu trebuie să fie igienizată. Fiți foarte atenți la utilizarea acestei abordări, totuși, deoarece puteți pierde evidența a ceea ce este suprascris în cazul în care.

Concluzie

În acest articol, am discutat despre elementele de bază ale metaprogramării în limba elixir. Am acoperit utilizarea citat, unquote, macro-uri și legături în timp ce vedeți câteva exemple și cazuri de utilizare. În acest moment, sunteți gata să aplicați aceste cunoștințe în practică și să creați programe mai concise și mai puternice. Amintiți-vă, totuși, că, de obicei, este mai bine să aveți un cod ușor de înțeles decât un cod concis, deci nu exagerați metaprogramarea în proiectele dvs..

Dacă doriți să aflați mai multe despre caracteristicile pe care le-am descris, nu ezitați să citiți ghidul oficial "Noțiuni de bază despre macrocomenzi", "citați" și "necotați". Sper că acest articol vă oferă o introducere plăcută în metaprogramarea în Elixir, care poate părea destul de complexă la început. În orice caz, nu vă fie frică să experimentați aceste noi instrumente!

Vă mulțumesc că ați rămas cu mine și vă voi vedea în curând.

Cod