Elixir Metaprogramming Grunnleggende

Metaprogrammering er en kraftig, men likevel kompleks teknikk, som betyr at et program kan analysere eller endres i løpet av kjøretiden. Mange moderne språk støtter denne funksjonen, og Elixir er ikke noe unntak. 

Med metaprogrammering kan du opprette nye komplekse makroer, dynamisk definere og utsette kodeutførelse, som lar deg skrive mer kortfattet og kraftig kode. Dette er faktisk et avansert emne, men forhåpentligvis etter å ha lest denne artikkelen får du en grunnleggende forståelse av hvordan du kommer i gang med metaprogrammering i Elixir.

I denne artikkelen vil du lære:

  • Hva det abstrakte syntakttreet er, og hvordan Elixir-koden er representert under hetten.
  • Hva i sitat og unquote funksjoner er.
  • Hvilke makroer er og hvordan du skal jobbe med dem.
  • Hvordan injisere verdier med binding.
  • Hvorfor makroer er hygieniske.

Før du begynner, la meg gi deg et lite råd. Husk Spider Man onkel sa "Med stor makt kommer stort ansvar"? Dette kan også brukes til metaprogrammering, fordi dette er en veldig kraftig funksjon som gjør at du kan vri og bøye kode til din vilje. 

Likevel må du ikke misbruke det, og du bør holde fast i enklere løsninger når det er sunt og mulig. For mye metaprogrammering kan gjøre koden mye vanskeligere å forstå og vedlikeholde, så vær forsiktig med det.

Abstract Syntax Tree og Sitat

Det første vi må forstå er hvordan vår Elixir-kode faktisk er representert. Disse representasjonene kalles ofte Abstract Syntax Trees (AST), men den offisielle Elixir-guiden anbefaler at de bare ringer dem siterte uttrykk

Det ser ut til at uttrykk kommer i form av tupler med tre elementer. Men hvordan kan vi bevise det? Vel, det heter en funksjon sitat som returnerer en representasjon for en gitt kode. I utgangspunktet gjør koden til en unevaluated form. For eksempel:

sitat gjør 1 + 2 ende # => : +, [kontekst: Elixir, import: Kernel], [1, 2]

Så hva skjer her? Tupelen returnert av sitat funksjonen har alltid følgende tre elementer:

  1. Atom eller en annen tuple med samme representasjon. I dette tilfellet er det et atom :+, noe som betyr at vi utfører tillegg. Forresten, denne form for skriveoperasjoner bør være kjent hvis du er kommet fra Ruby-verdenen.
  2. Søkeordsliste med metadata. I dette eksemplet ser vi at Kernel Modulen ble importert for oss automatisk.
  3. Liste over argumenter eller et atom. I dette tilfellet er dette en liste med argumentene 1 og 2.

Representasjonen kan være mye mer kompleks, selvfølgelig:

Sitat gjør Enum.each ([1,2,3], og (IO.puts (& 1))) ende # => :., [], [: __ aliases__, [alias: false], [: Enum ], hver], [], # [[1, 2, 3], # : &, [], # [:., [], [: __ aliases__, [alias: false] [: IO],: setter], [], # [: &, [], [1]]]]

På den annen side returnerer noen bokstaver seg når de sitert, spesielt:

  • atomer
  • heltall
  • flyter
  • lister
  • strenger
  • tuples (men bare med to elementer!)

I det neste eksemplet kan vi se at citerer et atom returnerer dette atom tilbake:

Sitat gjør: hei slutt # =>: hei

Nå som vi vet hvordan koden er representert under hetten, la oss gå videre til neste del og se hvilke makroer er og hvorfor sitert uttrykk er viktig.

makroer

Makroer er spesielle former som funksjoner, men de som returnerer sitert kode. Denne koden blir deretter plassert i applikasjonen, og utførelsen er utsatt. Det som er interessant er at makroer heller ikke evaluerer parametrene som er sendt til dem, de er også representert som sitater. Makroer kan brukes til å lage egendefinerte, komplekse funksjoner som brukes i hele prosjektet. 

Vær imidlertid oppmerksom på at makroene er mer komplekse enn vanlige funksjoner, og den offisielle guiden sier at de bare skal brukes som siste utvei. Med andre ord, hvis du kan ansette en funksjon, må du ikke opprette en makro fordi denne koden blir unødvendig kompleks og effektivt, vanskeligere å vedlikeholde. Makroene har likevel brukssaker, så la oss se hvordan du lager en.

Alt begynner med defmacro ring (som egentlig er en makro):

defmodule MyLib gjør defmacro test (arg) do arg |> IO.inspect ende ende

Denne makroen aksepterer bare et argument og skriver det ut.

Det er også verdt å nevne at makroer kan være private, akkurat som funksjoner. Private makroer kan bare kalles fra modulen der de ble definert. For å definere en slik makro, bruk defmacrop.

La oss nå lage en egen modul som skal brukes som vår lekeplass:

defmodule Main krever MyLib def start! gjør MyLib.test (1,2,3) slutten Main.start!

Når du kjører denne koden, : , [linje: 11], [1, 2, 3] vil bli skrevet ut, noe som faktisk betyr at argumentet har en sitert (uevaluert) form. Før jeg fortsetter, la meg imidlertid lage et lite notat.

Krev

Hvorfor i verden opprettet vi to separate moduler: en for å definere en makro og en annen til å kjøre prøvekoden? Det ser ut til at vi må gjøre det på denne måten, fordi makroer behandles før programmet utføres. Vi må også sørge for at den definerte makroen er tilgjengelig i modulen, og dette gjøres ved hjelp av krever. Denne funksjonen sikrer i utgangspunktet at den gitte modulen er kompilert før den nåværende.

Du kan spørre, hvorfor kan vi ikke bli kvitt hovedmodulen? La oss prøve å gjøre dette:

defmodul MyLib gjør defmacro test (arg) do arg |> IO.inspect ende ende MyLib.test (1,2,3) # => ** (UndefinedFunctionError) funksjon MyLib.test / 1 er udefinert eller privat. Men det er en makro med samme navn og arity. Sørg for å kreve MyLib hvis du har tenkt å påberope denne makroen # MyLib.test (1, 2, 3) # (elixir) lib / code.ex: 376: Code.require_file / 2

Dessverre får vi en feil som sier at funksjonstesten ikke kan bli funnet, selv om det er en makro med samme navn. Dette skjer fordi MittBibl Modulen er definert i samme omfang (og samme fil) der vi prøver å bruke den. Det kan virke litt rart, men husk bare at en egen modul skal opprettes for å unngå slike situasjoner.

Merk også at makroer ikke kan brukes globalt: først må du importere eller kreve den tilsvarende modulen.

Makroer og citerte uttrykk

Så vi vet hvordan Elixir uttrykk er representert internt og hvilke makroer er ... Nå hva? Nå, nå kan vi bruke denne kunnskapen og se hvordan den angitte koden kan evalueres.

La oss gå tilbake til makroene våre. Det er viktig å vite at siste uttrykk En hvilken som helst makro forventes å være en sitert kode som vil bli utført og returnert automatisk når makroen kalles. Vi kan omskrive eksemplet fra forrige seksjon ved å flytte IO.inspect til Hoved modul: 

defmodule MyLib gjør defmacro test (arg) gjør arg end-end defmodule Main krever MyLib def start! gjør MyLib.test (1,2,3) |> IO.inspect ende ende Main.start! # => 1, 2, 3

Se hva som skjer? Tuplen returnert av makroen er ikke sitert, men evaluert! Du kan prøve å legge til to heltall:

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

Nok en gang ble koden utført, og 3 ble returnert. Vi kan til og med prøve å bruke sitat Fungerer direkte, og den siste linjen vil fortsatt bli vurdert:

defmodule MyLib gjør defmacro test (arg) do arg |> IO.inspect quote gjør 1,2,3 slutten slutten # ... def start! gjør MyLib.test (1 + 2) |> IO.inspect # => : +, [linje: 14], [1, 2] # 1, 2, 3 enden

De arg ble sitert (merk, forresten, at vi selv kan se linjenummeret der makroen ble kalt), men det siterte uttrykket med tupelen 1,2,3 ble evaluert for oss, da dette er den siste linjen i makroen.

Vi kan bli fristet til å prøve å bruke arg i et matematisk uttrykk:

 defmacro test (arg) gjør sitat gjør arg + 1 ende ende

Men dette vil gi en feil som sier det arg eksisterer ikke. Hvorfor det? Dette er fordi arg er bokstavelig talt satt inn i strengen som vi sitater. Men det vi vil gjerne gjøre i stedet, er å evaluere arg, sett inn resultatet i strengen, og utfør deretter sitatet. For å gjøre dette, trenger vi enda en funksjon kalt unquote.

Unquoting koden

unquote er en funksjon som injiserer resultatet av kodevaluering inne i koden som vil bli citert. Dette kan høres litt bisarrt ut, men i virkeligheten er det ganske enkelt. La oss justere forrige kodeeksempel:

 defmacro test (arg) gjør sitat ikke unquote (arg) + 1 ende ende

Nå skal vårt program komme tilbake 4, som er akkurat det vi ønsket! Hva skjer er at koden passert til unquote Funksjonen kjøres bare når den oppgitte koden blir utført, ikke når den først blir analysert.

La oss se et litt mer komplekst eksempel. Anta at vi vil lage en funksjon som kjører noe uttrykk hvis den oppgitte strengen er et palindrom. Vi kunne skrive noe slikt:

 def if_palindrome_f? (str, expr) gjør hvis str == String.reverse (str), gjør: expr end

De _F suffiks her betyr at dette er en funksjon som senere vil vi opprette en lignende makro. Men hvis vi prøver å kjøre denne funksjonen nå, skrives teksten ut selv om strengen ikke er et palindrom:

 def start! gjør MyLib.if_palindrome_f? ("745", IO.puts ("yes")) # => "ja" slutt

Argumentene som sendes til funksjonen, blir evaluert før funksjonen faktisk kalles, så vi ser "ja" streng trykt ut på skjermen. Dette er faktisk ikke det vi ønsker å oppnå, så la oss prøve å bruke en makro i stedet:

 defmacro if_palindrome? (str, expr) gjør sitat hvis (unquote (str) == String.reverse (unquote (str))) gjør unquote (expr) ende ende ende # ... MyLib.if_palindrome? ("745", IO. puts ( "ja"))

Her citerer vi koden som inneholder hvis tilstand og bruk unquote inne for å evaluere verdiene av argumentene når makroen faktisk kalles. I dette eksemplet skrives ingenting ut på skjermen, noe som er riktig!

Injiserer verdier med bindinger

Ved hjelp av unquote er ikke den eneste måten å injisere kode på i en sitert blokk. Vi kan også benytte en funksjon som kalles bindende. Egentlig er dette ganske enkelt et alternativ bestått til sitat funksjon som aksepterer en søkeordliste med alle variablene som skal være unoterte bare én gang.

For å utføre bindende, pass bind_quoted til sitat fungere som dette:

sitat bind_quoted: [expr: expr] gjør slutt

Dette kan komme til nytte når du vil at uttrykket som brukes på flere steder, skal vurderes bare én gang. Som vist i dette eksempelet, kan vi lage en enkel makro som utdataer en streng to ganger med en forsinkelse på to sekunder:

defmodule MyLib gjør defmacro test (arg) gjør sitat bind_quoted: [arg: arg] do arg |> IO.inspect Process.sleep 2000 arg |> IO.inspect ende ende ende

Nå, hvis du kaller det ved å overføre systemtid, vil de to linjene ha det samme resultatet:

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

Dette er ikke tilfelle med unquote, fordi argumentet vil bli evaluert to ganger med en liten forsinkelse, så resultatene er ikke det samme:

 defmacro test (arg) do quote gjør unquote (arg) |> IO.inspect Process.sleep (2000) unquote (arg) |> IO.inspect ende ende # ... def start! gjør: os.system_time |> MyLib.test # => 1547457934011392 # => 1547457936059392 slutt

Konvertering av oppgitt kode

Noen ganger kan det være lurt å forstå hva koden din egentlig ser ut til for å feilsøke den, for eksempel. Dette kan gjøres ved å bruke to_string funksjon:

 defmacro if_palindrome? (str, expr) citerer = sitat gjør hvis (unquote (str) == String.reverse (unquote (str))) gjør unquote (expr) endende sitert |> Macro.to_string |> IO.inspect sitert slutt

Den trykte strengen vil være:

"hvis (\" 745 \ "== String.reverse (\" 745 \ ")) gjør \ n IO.puts (\" yes \ ") \ nend"

Vi kan se det gitte str Argumentet ble evaluert, og resultatet ble satt inn rett inn i koden. \ n her betyr "ny linje".

 Vi kan også utvide den angitte koden ved hjelp av expand_once og utvide:

 def start! citerer = sitat gjør MyLib.if_palindrome? ("745", IO.puts ("yes")) endert sitert |> Macro.expand_once (__ ENV__) |> IO.inspect end

Som produserer:

: hvis [kontekst: MyLib, import: Kernel], [: ==, [kontekst: MyLib, import: Kernel], ["745", :., [], [: __ aliases__, [alias : false, counter: -576460752303423103], [: String],: omvendt], [], ["745"]], [gjør: :., [], [: __ aliases__, [alias : false, counter: -576460752303423103], [: IO],: setter], [], ["ja"]]]

Selvfølgelig kan denne citerte representasjonen vendes tilbake til en streng:

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

Vi får samme resultat som før:

"hvis (\" 745 \ "== String.reverse (\" 745 \ ")) gjør \ n IO.puts (\" yes \ ") \ nend"

De utvide funksjonen er mer kompleks da den prøver å utvide hver makro i en gitt kode:

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

Resultatet vil være:

"case (\" 745 \ "== String.reverse (\" 745 \ ")) gjør \ nx når x i [false, nil] -> \ n nil \ n _ -> \ n IO.puts ja \ ") \ Nend"

Vi ser denne utgangen fordi hvis er egentlig en makro selv som er avhengig av sak uttalelse, så det blir utvidet også.

I disse eksemplene, __ENV__ er en spesiell form som returnerer miljøinformasjon som gjeldende modul, fil, linje, variabel i nåværende omfang og import.

Makroer er hygieniske

Du har kanskje hørt at makroer faktisk er hygienisk. Dette betyr at de ikke overskriver noen variabler utenfor deres omfang. For å bevise det, la oss legge til en prøvevariabel, prøv å endre verdien på forskjellige steder, og utdata den deretter:

 defmacro if_palindrome? (str, expr) gjør other_var = "if_palindrome?" sitert = sitat gjør other_var = "quoted" hvis (unquote (str) == String.reverse (unquote (str))) unquote (expr) end other_var |> IO.inspect end other_var |> IO.inspect citerte slutt # ... def start! gjør other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect end

Så other_var ble gitt en verdi inne i start! funksjon, inne i makroen, og inne i sitat. Du ser følgende utgang:

"If_palindrome?" "sitert" "start!"

Dette betyr at våre variabler er uavhengige, og vi introduserer ikke noen konflikter ved å bruke samme navn overalt (selvfølgelig, det ville være bedre å holde seg borte fra en slik tilnærming). 

Hvis du virkelig trenger å endre utvendig variabel fra en makro, kan du bruke den Var! som dette:

 defmacro if_palindrome? (str, expr) citerer = quote do var! (other_var) = "citerte" hvis (unquote (str) == String.reverse (unquote (str))) gjør unquote (expr) ... def start! gjør other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("yes")) other_var |> IO.inspect # => "sitert" slutten

Ved bruk av Var!, Vi sier faktisk at den oppgitte variabelen ikke skal hygieniseres. Vær imidlertid veldig forsiktig med å bruke denne tilnærmingen, da du kanskje mister oversikten over hva som blir overskrevet der.

Konklusjon

I denne artikkelen har vi diskutert metaprogrammeringsgrunnleggende i Elixir-språket. Vi har dekket bruken av sitat, unquote, makroer og bindinger mens du ser noen eksempler og bruker saker. På dette tidspunktet er du klar til å bruke denne kunnskapen i praksis og skape mer konsise og kraftige programmer. Husk imidlertid at det vanligvis er bedre å ha forståelig kode enn kortfattet kode, så bruk ikke for mye bruk av metaprogrammering i prosjektene dine.

Hvis du vil lære mer om funksjonene jeg har beskrevet, kan du lese den offisielle Komme i gang-guiden om makroer, sitater og unquote. Jeg håper virkelig denne artikkelen ga deg en fin introduksjon til metaprogrammering i Elixir, som faktisk kan virke ganske komplisert først. I alle fall, vær ikke redd for å eksperimentere med disse nye verktøyene!

Jeg takker for at du bodde hos meg og ser deg snart.