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:
sitat
og unquote
funksjoner er.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.
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:
:+
, 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.Kernel
Modulen ble importert for oss automatisk.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:
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 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.
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.
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
.
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!
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
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.
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.
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.