Un'opinione dissenziente: l'omoiconicità di Lisp è molto meno utile di quanto molti fan di Lisp vorrebbero farti credere.
Per comprendere le macro sintattiche, è importante comprendere i compilatori. Il compito di un compilatore è trasformare il codice leggibile in codice eseguibile. Da una prospettiva di altissimo livello, ci sono due fasi generali: analisi e generazione di codice .
L'analisi è il processo di lettura del codice, interpretazione secondo una serie di regole formali e trasformazione in una struttura ad albero, generalmente nota come AST (Abstract Syntax Tree). Per tutta la diversità tra i linguaggi di programmazione, questa è una notevole comunanza: essenzialmente ogni linguaggio di programmazione per scopi generici analizza una struttura ad albero.
La generazione del codice prende l'AST del parser come input e lo trasforma in codice eseguibile tramite l'applicazione di regole formali. Dal punto di vista delle prestazioni, questo è un compito molto più semplice; molti compilatori di linguaggio di alto livello impiegano il 75% o più del loro tempo per l'analisi.
La cosa da ricordare di Lisp è che è molto, molto vecchio. Tra i linguaggi di programmazione, solo FORTRAN è più vecchio di Lisp. Nel lontano passato, l'analisi (la parte lenta della compilazione) era considerata un'arte oscura e misteriosa. Gli articoli originali di John McCarthy sulla teoria di Lisp (quando era solo un'idea che non avrebbe mai pensato potesse essere effettivamente implementato come un vero linguaggio di programmazione per computer) descrivono una sintassi un po 'più complessa ed espressiva rispetto alle moderne "S-espressioni ovunque per tutto "notazione. Ciò è avvenuto più tardi, quando le persone stavano cercando di implementarlo. Dato che all'epoca l'analisi non era ben compresa, in pratica hanno puntato su di essa e hanno attenuato la sintassi in una struttura ad albero omoiconico per rendere il lavoro del parser assolutamente banale. Il risultato finale è che tu (lo sviluppatore) devi fare molto del parser " s lavoraci scrivendo l'AST formale nel tuo codice. L'omoiconicità non "rende le macro molto più facili" tanto quanto rende molto più difficile scrivere tutto il resto!
Il problema è che, specialmente con la tipizzazione dinamica, è molto difficile per le espressioni S trasportare molte informazioni semantiche con sé. Quando tutta la sintassi è dello stesso tipo di cose (elenchi di elenchi), non c'è molto nel modo di contesto fornito dalla sintassi, e quindi il sistema macro ha molto poco con cui lavorare.
La teoria dei compilatori ha fatto molta strada dagli anni '60 quando inventò Lisp e, sebbene le cose che realizzò fossero impressionanti ai suoi tempi, ora sembrano piuttosto primitive. Per un esempio di un moderno sistema di metaprogrammazione, dai un'occhiata al linguaggio Boo (tristemente sottovalutato). Boo è tipicamente statico, orientato agli oggetti e open-source, quindi ogni nodo AST ha un tipo con una struttura ben definita su cui uno sviluppatore di macro può leggere il codice. Il linguaggio ha una sintassi relativamente semplice ispirata a Python, con varie parole chiave che danno un significato semantico intrinseco alle strutture ad albero costruite da esse, e la sua metaprogrammazione ha una sintassi quasiquote intuitiva per semplificare la creazione di nuovi nodi AST.
Ecco una macro che ho creato ieri quando mi sono reso conto che stavo applicando lo stesso modello a un sacco di posizioni diverse nel codice GUI, in cui avrei richiamato BeginUpdate()
un controllo UI, avrei eseguito un aggiornamento in un try
blocco e quindi avrei chiamato EndUpdate()
:
macro UIUpdate(value as Expression):
return [|
$value.BeginUpdate()
try:
$(UIUpdate.Body)
ensure:
$value.EndUpdate()
|]
Il macro
comando è, infatti, una macro stessa , che accetta un corpo macro come input e genera una classe per elaborare la macro. Utilizza il nome della macro come variabile che rappresenta il MacroStatement
nodo AST che rappresenta la chiamata della macro. Il [| ... |] è un blocco di quasiquote, che genera l'AST corrispondente al codice all'interno e all'interno del blocco di quasiquote, il simbolo $ fornisce la funzione "non quotata", sostituendo in un nodo come specificato.
Con questo, è possibile scrivere:
UIUpdate myComboBox:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
e fallo espandere a:
myComboBox.BeginUpdate()
try:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
ensure:
myComboBox.EndUpdate()
Esprimendo la macro in questo modo è più semplice e più intuitiva di quanto lo sarebbe in una macro Lisp, perché lo sviluppatore conosce la struttura di MacroStatement
e sa come la Arguments
e Body
le proprietà di lavoro, e che la conoscenza intrinseca può essere utilizzato per esprimere i concetti coinvolti in modo molto intuitivo modo. È anche più sicuro, perché il compilatore conosce la struttura di MacroStatement
e se provi a codificare qualcosa che non è valido per a MacroStatement
, il compilatore lo catturerà immediatamente e segnalerà l'errore invece di te che non lo sai fino a quando qualcosa ti esplode in runtime.
L'innesto di macro su Haskell, Python, Java, Scala, ecc. Non è difficile perché queste lingue non sono omoiconiche; è difficile perché le lingue non sono progettate per loro e funziona meglio quando la gerarchia AST della tua lingua è progettata da zero per essere esaminata e manipolata da un sistema macro. Quando lavori con un linguaggio progettato pensando alla metaprogrammazione fin dall'inizio, le macro sono molto più semplici e facili da usare!