Nozioni di base sull'elisir Metaprogramming

Metaprogramming è una tecnica potente, ma piuttosto complessa, il che significa che un programma può analizzarsi o addirittura modificarsi durante il runtime. Molte lingue moderne supportano questa funzione e l'elisir non fa eccezione. 

Con metaprogrammazione, è possibile creare nuove macro complesse, definire dinamicamente e rinviare l'esecuzione del codice, che consente di scrivere codice più conciso e potente. Questo è davvero un argomento avanzato, ma si spera che dopo aver letto questo articolo si ottenga una comprensione di base su come iniziare con metaprogrammazione in elisir.

In questo articolo imparerai:

  • Che cos'è l'albero sintattico astratto e come il codice Elixir è rappresentato sotto il cofano.
  • Che cosa citazione e chiudere le virgolette le funzioni sono.
  • Quali macro sono e come lavorare con loro.
  • Come iniettare valori con associazione.
  • Perché le macro sono igieniche.

Prima di iniziare, tuttavia, lascia che ti dia un piccolo consiglio. Ricorda lo zio di Spider Man ha detto: "Con un grande potere derivano grandi responsabilità"? Questo può essere applicato anche a metaprogrammazione perché questa è una funzionalità molto potente che ti permette di ruotare e piegare il codice a tuo piacimento. 

Tuttavia, non devi abusarne e dovresti attenersi a soluzioni più semplici quando è sano e possibile. Troppa metaprogrammazione potrebbe rendere il tuo codice molto più difficile da capire e mantenere, quindi fai attenzione.

Sintassi albero astratto e Citazione

La prima cosa che dobbiamo capire è come viene rappresentato il nostro codice Elixir. Queste rappresentazioni sono spesso chiamate Abstract Syntax Trees (AST), ma la guida ufficiale di Elixir consiglia di chiamarle semplicemente espressioni citate

Sembra che le espressioni arrivino sotto forma di tuple con tre elementi. Ma come possiamo dimostrarlo? Bene, c'è una funzione chiamata citazione che restituisce una rappresentazione per un determinato codice. Fondamentalmente, trasforma il codice in un forma non valutata. Per esempio:

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

Allora, cosa sta succedendo qui? La tupla restituita dal citazione la funzione ha sempre i seguenti tre elementi:

  1. Atomo o un'altra tupla con la stessa rappresentazione. In questo caso, è un atomo :+, nel senso che stiamo facendo l'aggiunta. A proposito, questa forma di scrittura dovrebbe essere familiare se vieni dal mondo Ruby.
  2. Elenco di parole chiave con metadati. In questo esempio vediamo che il nocciolo il modulo è stato importato automaticamente per noi.
  3. Elenco di argomenti o un atomo. In questo caso, questa è una lista con gli argomenti 1 e 2.

La rappresentazione può essere molto più complessa, ovviamente:

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

D'altra parte, alcuni letterali si restituiscono quando citati, in particolare:

  • atomi
  • interi
  • galleggianti
  • liste
  • stringhe
  • tuple (ma solo con due elementi!)

Nel prossimo esempio, possiamo vedere che la citazione di un atomo restituisce questo atomo:

quote do: hi end # =>: ciao

Ora che sappiamo come viene rappresentato il codice sotto il cofano, passiamo alla sezione successiva e vediamo quali macro sono e perché le espressioni citate sono importanti.

Macro

Le macro sono forme speciali come le funzioni, ma quelle che restituiscono il codice citato. Questo codice viene quindi inserito nell'applicazione e la sua esecuzione viene posticipata. La cosa interessante è che anche le macro non valutano i parametri passati a loro-sono anche rappresentate come espressioni quotate. I macro possono essere utilizzati per creare funzioni personalizzate e complesse utilizzate nel progetto. 

Tenete presente, tuttavia, che le macro sono più complesse delle funzioni regolari e che la guida ufficiale afferma che dovrebbero essere utilizzate solo come ultima risorsa. In altre parole, se è possibile utilizzare una funzione, non creare una macro perché in questo modo il codice diventa inutilmente complesso e, in effetti, più difficile da mantenere. Tuttavia, i macro hanno i loro casi d'uso, quindi vediamo come crearne uno.

Tutto inizia con il defmacro call (che in realtà è una macro stessa):

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

Questa macro accetta semplicemente un argomento e lo stampa.

Inoltre, vale la pena ricordare che le macro possono essere private, proprio come le funzioni. Le macro private possono essere chiamate solo dal modulo in cui sono state definite. Per definire una tale macro, usare defmacrop.

Ora creiamo un modulo separato che verrà utilizzato come nostro parco giochi:

defmodule Main richiede MyLib def start! fai MyLib.test (1,2,3) end end Main.start!

Quando si esegue questo codice, : , [riga: 11], [1, 2, 3] verrà stampato, il che significa che l'argomento ha una forma quotata (non valutata). Prima di procedere, tuttavia, lasciatemi fare una piccola nota.

Richiedere

Perché nel mondo abbiamo creato due moduli separati: uno per definire una macro e un altro per eseguire il codice di esempio? Sembra che dobbiamo farlo in questo modo, perché le macro vengono elaborate prima dell'esecuzione del programma. Dobbiamo anche assicurarci che la macro definita sia disponibile nel modulo, e ciò viene fatto con l'aiuto di richiedere. Questa funzione, fondamentalmente, assicura che il modulo specificato sia compilato prima di quello corrente.

Potresti chiedere, perché non possiamo sbarazzarci del modulo principale? Proviamo a fare questo:

defmodule MyLib do defmacro test (arg) do arg |> IO.inspect end end MyLib.test (1,2,3) # => ** (UndefinedFunctionError) function MyLib.test / 1 non è definito o privato. Tuttavia c'è una macro con lo stesso nome e arità. Assicurati di richiedere MyLib se intendi richiamare questa macro # MyLib.test (1, 2, 3) # (elixir) lib / code.ex: 376: Code.require_file / 2

Sfortunatamente, riceviamo un errore che dice che non è stato possibile trovare il test funzionale, sebbene esista una macro con lo stesso nome. Questo succede perché il MyLib il modulo è definito nello stesso ambito (e nello stesso file) dove stiamo cercando di usarlo. Può sembrare un po 'strano, ma per ora basta ricordare che un modulo separato dovrebbe essere creato per evitare tali situazioni.

Si noti inoltre che le macro non possono essere utilizzate globalmente: per prima cosa è necessario importare o richiedere il modulo corrispondente.

Macro e espressioni citate

Quindi sappiamo come le espressioni di Elisir sono rappresentate internamente e quali sono le macro ... E adesso? Bene, ora possiamo utilizzare questa conoscenza e vedere come può essere valutato il codice quotato.

Torniamo ai nostri macro. È importante sapere che il ultima espressione ci si aspetta che ogni macro sia un codice quotato che verrà eseguito e restituito automaticamente quando viene chiamata la macro. Possiamo riscrivere l'esempio della sezione precedente spostandoci IO.inspect al Principale modulo: 

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

Guarda cosa succede? La tupla restituita dalla macro non viene citata ma valutata! Puoi provare ad aggiungere due numeri interi:

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

Ancora una volta, il codice è stato eseguito e 3 è stato restituito. Possiamo persino provare a usare il citazione funzione direttamente, e l'ultima riga sarà ancora valutata:

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

Il arg è stato citato (si noti, a proposito, che possiamo anche vedere il numero di riga in cui è stata chiamata la macro), ma l'espressione citata con la tupla 1,2,3 è stato valutato per noi in quanto questa è l'ultima riga della macro.

Potremmo essere tentati di provare a usare il arg in un'espressione matematica:

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

Ma questo solleverà un errore dicendo questo arg non esiste. Perchè così? Questo è perché arg è letteralmente inserito nella stringa che citiamo. Ma quello che vorremmo fare invece è valutare il arg, inserisci il risultato nella stringa, quindi esegui il preventivo. Per fare ciò, avremo bisogno di un'altra funzione chiamata chiudere le virgolette.

Smantellamento del codice

chiudere le virgolette è una funzione che inietta il risultato della valutazione del codice all'interno del codice che verrà quindi quotato. Questo può sembrare un po 'bizzarro, ma in realtà le cose sono abbastanza semplici. Analizziamo l'esempio di codice precedente:

 defmacro test (arg) cita do unquote (arg) + 1 end end

Ora il nostro programma sta per tornare 4, che è esattamente quello che volevamo! Quello che succede è che il codice è passato al chiudere le virgolette la funzione viene eseguita solo quando viene eseguito il codice quotato, non quando viene analizzato inizialmente.

Vediamo un esempio leggermente più complesso. Supponiamo di voler creare una funzione che esegua un'espressione se la stringa data è un palindromo. Potremmo scrivere qualcosa come questo:

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

Il _f suffisso qui significa che questa è una funzione poiché in seguito creeremo una macro simile. Tuttavia, se proviamo a eseguire questa funzione ora, il testo verrà stampato anche se la stringa non è un palindromo:

 def start! do MyLib.if_palindrome_f? ("745", IO.puts ("si")) # => "sì" fine

Gli argomenti passati alla funzione vengono valutati prima che la funzione venga effettivamente chiamata, quindi vediamo il "sì" stringa stampata sullo schermo. Questo non è esattamente ciò che vogliamo ottenere, quindi proviamo a utilizzare una macro invece:

 defmacro if_palindrome? (str, expr) do cita do if (unquote (str) == String.reverse (unquote (str))) do unquote (expr) end end end # ... MyLib.if_palindrome? ("745", IO. puts ( "sì"))

Qui stiamo citando il codice che contiene il Se condizione e uso chiudere le virgolette all'interno per valutare i valori degli argomenti quando viene effettivamente chiamata la macro. In questo esempio, nulla verrà stampato sullo schermo, che è corretto!

Iniezione di valori con attacchi

utilizzando chiudere le virgolette non è l'unico modo per iniettare il codice in un blocco quotato. Possiamo anche utilizzare una funzione chiamata rilegatura. In realtà, questa è semplicemente un'opzione passata al citazione funzione che accetta un elenco di parole chiave con tutte le variabili che dovrebbero essere non quotate solo una volta.

Per eseguire il binding, passare bind_quoted al citazione funzione come questa:

quote bind_quoted: [expr: expr] do end

Questo può tornare utile quando si desidera che l'espressione utilizzata in più punti venga valutata una sola volta. Come dimostrato da questo esempio, possiamo creare una semplice macro che restituisce una stringa due volte con un ritardo di due secondi:

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

Ora, se lo chiami passando il tempo di sistema, le due linee avranno lo stesso risultato:

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

Questo non è il caso con chiudere le virgolette, perché l'argomento verrà valutato due volte con un piccolo ritardo, quindi i risultati non sono gli stessi:

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

Conversione del codice quotato

A volte, potresti voler capire come appare il tuo codice quotato, per esempio, per eseguirne il debug. Questo può essere fatto usando il accordare funzione:

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

La stringa stampata sarà:

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

Possiamo vedere che il dato str argomento è stato valutato, e il risultato è stato inserito direttamente nel codice. \ n qui significa "nuova linea".

 Inoltre, possiamo espandere il codice quotato usando expand_once e espandere:

 def start! do quoted = quote do MyLib.if_palindrome? ("745", IO.puts ("si")) fine quotata |> Macro.expand_once (__ ENV__) |> IO.inspect fine

Che produce:

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

Naturalmente, questa rappresentazione citata può essere riportata a una stringa:

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

Otterremo lo stesso risultato di prima:

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

Il espandere la funzione è più complessa in quanto tenta di espandere ogni macro in un dato codice:

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

Il risultato sarà:

"case (\" 745 \ "== String.reverse (\" 745 \ ")) fa \ nx quando x in [false, nil] -> \ n nil \ n _ -> \ n IO.puts (\" Sì \ ") \ Nend"

Vediamo questo risultato perché Se è in realtà una macro che si basa sul Astuccio affermazione, quindi anche questa viene espansa.

In questi esempi, __ENV__ è un modulo speciale che restituisce informazioni sull'ambiente come il modulo corrente, il file, la linea, la variabile nell'ambito corrente e le importazioni.

Le macro sono igieniche

Potresti aver sentito che i macro sono in realtà igienico. Ciò significa che non sovrascrivono alcuna variabile al di fuori del loro ambito. Per dimostrarlo, aggiungiamo una variabile di esempio, prova a cambiarne il valore in vari punti e poi pubblicala:

 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.inspect end other_var |> IO.inspect quoted end # ... def start! do other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("si")) other_var |> IO.inspect fine

Così other_var è stato dato un valore all'interno del inizio! funzione, all'interno della macro e all'interno di citazione. Vedrai il seguente output:

"If_palindrome?" "quotato" "inizio!"

Ciò significa che le nostre variabili sono indipendenti e non stiamo introducendo alcun conflitto usando lo stesso nome ovunque (anche se, ovviamente, sarebbe meglio evitare questo approccio). 

Se è davvero necessario modificare la variabile esterna all'interno di una macro, è possibile utilizzarla var! come questo:

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

Usando var!, stiamo effettivamente dicendo che la variabile data non dovrebbe essere igienizzata. Prestare molta attenzione all'utilizzo di questo approccio, tuttavia, poiché si potrebbe perdere traccia di ciò che viene sovrascritto dove.

Conclusione

In questo articolo, abbiamo discusso le basi della metaprogrammazione nella lingua dell'elisir. Abbiamo coperto l'utilizzo di citazione, chiudere le virgolette, macro e collegamenti durante la visualizzazione di alcuni esempi e casi d'uso. A questo punto, sei pronto per applicare queste conoscenze nella pratica e creare programmi più concisi e potenti. Ricorda, tuttavia, che di solito è meglio avere un codice comprensibile rispetto al codice conciso, quindi non usare eccessivamente la metaprogrammazione nei tuoi progetti.

Se desideri saperne di più sulle funzionalità che ho descritto, non esitare a leggere la guida introduttiva ufficiale su macro, quotazioni e unquote. Spero davvero che questo articolo ti abbia dato una bella introduzione alla metaprogrammazione in Elisir, che all'inizio può sembrare piuttosto complessa. In ogni caso, non aver paura di sperimentare con questi nuovi strumenti!

Ti ringrazio per essere stato con me, e a presto.