Codice di scrittura del codice: un'introduzione alla teoria e alla pratica della metaprogrammazione moderna

Pubblicato: 2022-07-22

Ogni volta che penso al modo migliore per spiegare le macro, ricordo un programma Python che ho scritto quando ho iniziato a programmare. Non riuscivo a organizzarlo come avrei voluto. Ho dovuto chiamare una serie di funzioni leggermente diverse e il codice è diventato ingombrante. Quello che stavo cercando, anche se allora non lo sapevo, era la metaprogrammazione .

metaprogrammazione (sostantivo)

Qualsiasi tecnica con cui un programma può trattare il codice come dati.

Possiamo costruire un esempio che dimostri gli stessi problemi che ho dovuto affrontare con il mio progetto Python immaginando di creare il back-end di un'app per i proprietari di animali domestici. Usando gli strumenti in una libreria, pet_sdk , scriviamo Python per aiutare i proprietari di animali domestici ad acquistare cibo per gatti:

 import pet_sdk cats = pet_sdk.get_cats() print(f"Found {len(cats)} cats!") for cat in cats: pet_sdk.order_cat_food(cat, amount=cat.food_needed)
Snippet 1: Ordina cibo per gatti

Dopo aver confermato che il codice funziona, si passa all'implementazione della stessa logica per altri due tipi di animali domestici (uccelli e cani). Aggiungiamo anche una funzione per prenotare gli appuntamenti dal veterinario:

 # An SDK that can give us information about pets - unfortunately, the functions are slightly different for each pet import pet_sdk # Get all of the birds, cats, and dogs in the system, respectively birds = pet_sdk.get_birds() cats = pet_sdk.get_cats() dogs = pet_sdk.get_dogs() for cat in cats: print(f"Checking information for cat {cat.name}") if cat.hungry(): pet_sdk.order_cat_food(cat, amount=cat.food_needed) cat.clean_litterbox() if cat.sick(): available_vets = pet_sdk.find_vets(animal="cat") if len(available_vets) > 0: vet = available_vets[0] vet.book_cat_appointment(cat) for dog in dogs: print(f"Checking information for dog {dog.name}") if dog.hungry(): pet_sdk.order_dog_food(dog, amount=dog.food_needed) dog.walk() if dog.sick(): available_vets = pet_sdk.find_vets(animal="dog") if len(available_vets) > 0: vet = available_vets[0] vet.book_dog_appointment(dog) for bird in birds: print(f"Checking information for bird {bird.name}") if bird.hungry(): pet_sdk.order_bird_food(bird, amount=bird.food_needed) bird.clean_cage() if bird.sick(): available_vets = pet_sdk.find_birds(animal="bird") if len(available_vets) > 0: vet = available_vets[0] vet.book_bird_appointment(bird)
Snippet 2: Ordina cibo per gatti, cani e uccelli; Prenota appuntamento dal veterinario

Sarebbe utile condensare la logica ripetitiva di Snippet 2 in un ciclo, quindi abbiamo deciso di riscrivere il codice. Ci rendiamo subito conto che, poiché ogni funzione ha un nome diverso, non possiamo determinare quale (ad esempio, book_bird_appointment , book_cat_appointment ) chiamare nel nostro ciclo:

 import pet_sdk all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: # What now?
Frammento 3: E adesso?

Immaginiamo una versione turbo di Python in cui possiamo scrivere programmi che generano automaticamente il codice finale che vogliamo, uno in cui possiamo manipolare in modo flessibile, semplice e fluido il nostro programma come se fosse un elenco, dati in un file o qualsiasi altro tipo di dati comune o input di programma:

 import pet_sdk for animal in ["cat", "dog", "bird"]: animals = pet_sdk.get_{animal}s() # When animal is "cat", this # would be pet_sdk.get_cats() for animal in animal: pet_sdk.order_{animal}_food(animal, amount=animal.food_needed) # When animal is "dog" this would be # pet_sdk.order_dog_food(dog, amount=dog.food_needed)
Snippet 4: TurboPython: un programma immaginario

Questo è un esempio di macro , disponibile in linguaggi come Rust, Julia o C, solo per citarne alcuni, ma non Python.

Questo scenario è un ottimo esempio di come potrebbe essere utile scrivere un programma in grado di modificare e manipolare il proprio codice. Questo è precisamente il disegno delle macro, ed è una delle tante risposte a una domanda più grande: come possiamo fare in modo che un programma introspezioni il proprio codice, lo trattino come dati, e poi agisca in base a quell'introspezione?

In generale, tutte le tecniche che possono realizzare tale introspezione rientrano nel termine generale di "metaprogrammazione". La metaprogrammazione è un ricco sottocampo nella progettazione del linguaggio di programmazione e può essere ricondotta a un concetto importante: il codice come dati.

Riflessione: in difesa di Python

Potresti sottolineare che, sebbene Python possa non fornire supporto per le macro, offre molti altri modi per scrivere questo codice. Ad esempio, qui utilizziamo il metodo isinstance() per identificare la classe di cui la nostra variabile animal è un'istanza e chiamiamo la funzione appropriata:

 # An SDK that can give us information about pets - unfortunately, the functions # are slightly different import pet_sdk def process_animal(animal): if isinstance(animal, pet_sdk.Cat): animal_name_type = "cat" order_food_fn = pet_sdk.order_cat_food care_fn = animal.clean_litterbox elif isinstance(animal, pet_sdk.Dog): animal_name_type = "dog" order_food_fn = pet_sdk.order_dog_food care_fn = animal.walk elif isinstance(animal, pet_sdk.Bird): animal_name_type = "bird" order_food_fn = pet_sdk.order_bird_food care_fn = animal.clean_cage else: raise TypeError("Unrecognized animal!") print(f"Checking information for {animal_name_type} {animal.name}") if animal.hungry(): order_food_fn(animal, amount=animal.food_needed) care_fn() if animal.sick(): available_vets = pet_sdk.find_vets(animal=animal_name_type) if len(available_vets) > 0: vet = available_vets[0] # We still have to check again what type of animal it is if isinstance(animal, pet_sdk.Cat): vet.book_cat_appointment(animal) elif isinstance(animal, pet_sdk.Dog): vet.book_dog_appointment(animal) else: vet.book_bird_appointment(animal) all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: process_animal(animal)
Snippet 5: un esempio idiomatico

Chiamiamo questo tipo di metaprogrammazione riflessione e ci torneremo più avanti. Il codice di Snippet 5 è ancora un po' ingombrante ma più facile da scrivere per un programmatore rispetto a quello di Snippet 2, in cui abbiamo ripetuto la logica per ogni animale elencato.

Sfida

Utilizzando il metodo getattr , modificare il codice precedente per chiamare dinamicamente le funzioni order_*_food e book_*_appointment appropriate. Questo probabilmente rende il codice meno leggibile, ma se conosci bene Python, vale la pena pensare a come potresti usare getattr invece della funzione isinstance e semplificare il codice.


Omoiconicità: l'importanza del Lisp

Alcuni linguaggi di programmazione, come Lisp, portano il concetto di metaprogrammazione a un altro livello tramite l'omoiconicità .

omoiconicità (sostantivo)

La proprietà di un linguaggio di programmazione per cui non c'è distinzione tra codice e dati su cui sta operando un programma.

Lisp, creato nel 1958, è il più antico linguaggio omoiconico e il secondo più antico linguaggio di programmazione di alto livello. Prendendo il nome da "LISTt Processor", Lisp è stata una rivoluzione nell'informatica che ha profondamente plasmato il modo in cui i computer vengono utilizzati e programmati. È difficile sopravvalutare come Lisp abbia influenzato in modo fondamentale e distintivo la programmazione.

Emacs è scritto in Lisp, che è l'unico linguaggio per computer bello. Neal Stephenson

Lisp è stato creato solo un anno dopo FORTRAN, nell'era delle schede perforate e dei computer militari che riempivano una stanza. Eppure i programmatori usano ancora oggi Lisp per scrivere applicazioni nuove e moderne. Il principale creatore di Lisp, John McCarthy, è stato un pioniere nel campo dell'IA. Per molti anni, il Lisp è stato il linguaggio dell'IA, con i ricercatori che hanno apprezzato la capacità di riscrivere dinamicamente il proprio codice. La ricerca AI di oggi è incentrata su reti neurali e modelli statistici complessi, piuttosto che su quel tipo di codice di generazione logica. Tuttavia, la ricerca condotta sull'IA utilizzando Lisp, in particolare la ricerca condotta negli anni '60 e '70 al MIT e a Stanford, ha creato il campo come lo conosciamo e la sua enorme influenza continua.

L'avvento di Lisp ha esposto per la prima volta i primi programmatori alle pratiche possibilità computazionali di cose come la ricorsione, le funzioni di ordine superiore e le liste collegate. Ha anche dimostrato la potenza di un linguaggio di programmazione basato sulle idee del calcolo lambda.

Queste nozioni hanno innescato un'esplosione nella progettazione dei linguaggi di programmazione e, come ha affermato Edsger Dijkstra, uno dei più grandi nomi dell'informatica, " […] ha aiutato un certo numero dei nostri simili più dotati a pensare pensieri prima impossibili".

Questo esempio mostra un semplice programma Lisp (e il suo equivalente nella più familiare sintassi Python) che definisce una funzione "fattoriale" che calcola ricorsivamente il fattoriale del suo input e chiama quella funzione con l'input "7":

Liscio Pitone
( defun factorial ( n ) ( if ( = n 1 ) 1 ( * n ( factorial ( - n 1 ))))) ( print ( factorial 7 ))
 def factorial (n) : if n == 1 : return 1 else : return n * factorial(n -1 ) print(factorial( 7 ))

Codice come Dati

Nonostante sia una delle innovazioni più impattanti e consequenziali di Lisp, l'omoiconicità, a differenza della ricorsione e di molti altri concetti sperimentati da Lisp, non è entrata nella maggior parte dei linguaggi di programmazione odierni.

La tabella seguente confronta le funzioni omoiconiche che restituiscono codice sia in Julia che in Lisp. Julia è un linguaggio omoiconico che, per molti versi, assomiglia ai linguaggi di alto livello con cui potresti avere familiarità (es. Python, Ruby).

Il pezzo chiave della sintassi in ogni esempio è il suo carattere di virgolette . Julia usa un : (due punti) per citare, mentre Lisp usa un ' (virgoletta singola):

Giulia Liscio
function function_that_returns_code() return :(x + 1 ) end
 ( defun function_that_returns_code () '(+ x 1 ))

In entrambi gli esempi, la virgoletta accanto all'espressione principale ( (x + 1) o (+ x 1) ) la trasforma da codice che sarebbe stato valutato direttamente in un'espressione astratta che possiamo manipolare. La funzione restituisce il codice, non una stringa o dati. Se dovessimo chiamare la nostra funzione e scrivere print(function_that_returns_code()) , Julia stamperebbe il codice stringato come x+1 (e l'equivalente vale per Lisp). Al contrario, senza : (o ' in Lisp), otterremmo un errore che x non è stato definito.

Torniamo al nostro esempio di Julia ed estendiamolo:

 function function_that_returns_code(n) return :(x + $n) end my_code = function_that_returns_code(3) print(my_code) # Prints out (x + 3) x = 1 print(eval(my_code)) # Prints out 4 x = 3 print(eval(my_code)) # Prints out 6
Snippet 6: Esempio Julia esteso

La funzione eval può essere utilizzata per eseguire il codice che generiamo da un'altra parte del programma. Si noti che il valore stampato si basa sulla definizione della variabile x . Se provassimo a eval il nostro codice generato in un contesto in cui x non era definito, otterremmo un errore.

L'omoiconicità è un potente tipo di metaprogrammazione, in grado di sbloccare nuovi e complessi paradigmi di programmazione in cui i programmi possono adattarsi al volo, generando codice per adattarsi a problemi specifici del dominio o nuovi formati di dati incontrati.

Prendi il caso di WolframAlpha, dove l'omoiconico Wolfram Language può generare codice per adattarsi a un'incredibile gamma di problemi. Puoi chiedere a WolframAlpha: "Qual è il PIL di New York diviso per la popolazione di Andorra?" e, notevolmente, ricevere una risposta logica.

Sembra improbabile che qualcuno possa mai pensare di includere questo calcolo oscuro e inutile in un database, ma Wolfram usa la metaprogrammazione e un grafico della conoscenza ontologica per scrivere codice al volo per rispondere a questa domanda.

È importante comprendere la flessibilità e la potenza fornite dal Lisp e da altri linguaggi omoiconici. Prima di approfondire, consideriamo alcune delle opzioni di metaprogrammazione a tua disposizione:

Definizione Esempi Appunti
Omoiconicità Una caratteristica del linguaggio in cui il codice è dati di "prima classe". Poiché non c'è separazione tra codice e dati, i due possono essere usati in modo intercambiabile.
  • Liscio
  • Prologo
  • Giulia
  • Rebol/Rosso
  • Linguaggio Wolfram
Qui, Lisp include altre lingue nella famiglia Lisp, come Scheme, Racket e Clojure.
Macro Un'istruzione, una funzione o un'espressione che accetta il codice come input e restituisce il codice come output.
  • Le macro_rules! , Derive e macro procedurali
  • @macro di Julia
  • La defmacro di Lisp
  • C è #define
(Vedi la prossima nota sulle macro di C.)
Direttive del preprocessore (o precompilatore) Un sistema che accetta un programma come input e, in base alle istruzioni incluse nel codice, restituisce una versione modificata del programma come output.
  • Le macro di C
  • Il sistema di # preprocessore di C++
Le macro di C vengono implementate utilizzando il sistema del preprocessore di C, ma i due sono concetti separati.

La differenza concettuale chiave tra le macro di C (in cui usiamo la direttiva del preprocessore #define ) e altre forme di direttive del preprocessore C (ad esempio, #if e #ifndef ) è che usiamo le macro per generare codice mentre usiamo altri non #define direttive del preprocessore per compilare condizionalmente altro codice. I due sono strettamente correlati in C e in altri linguaggi, ma sono tipi diversi di metaprogrammazione.
Riflessione La capacità di un programma di esaminare, modificare e introspezione il proprio codice.
  • isinstance , getattr , functions di Python
  • JavaScript's Reflect e typeof
  • getDeclaredMethods di Java
  • Gerarchia di classi System.Type di .NET
La riflessione può verificarsi in fase di compilazione o in fase di esecuzione.
Generici La possibilità di scrivere codice valido per diversi tipi o che può essere utilizzato in più contesti ma archiviato in un'unica posizione. Possiamo definire i contesti in cui il codice è valido in modo esplicito o implicito.

Generici in stile modello:

  • C++
  • Ruggine
  • Giava

Polimorfismo parametrico:

  • Haskell
  • ML
La programmazione generica è un argomento più ampio della metaprogrammazione generica e il confine tra i due non è ben definito.

Dal punto di vista di questo autore, un sistema di tipi parametrici conta come metaprogrammazione solo se è in un linguaggio tipizzato staticamente.
Un riferimento per la metaprogrammazione

Diamo un'occhiata ad alcuni esempi pratici di omoiconicità, macro, direttive del preprocessore, riflessione e generici scritti in vari linguaggi di programmazione:

 # Prints out "Hello Will", "Hello Alice", by dynamically creating the lines of code say_hi = :(println("Hello, ", name)) name = "Will" eval(say_hi) name = "Alice" eval(say_hi)
Snippet 7: Omoiconicità in Julia
 int main() { #ifdef _WIN32 printf("This section will only be compiled for and run on windows!\n"); windows_only_function(); #elif __unix__ printf("This section will only be compiled for and run on unix!\n"); unix_only_function(); #endif printf("This line runs regardless of platform!\n"); return 1; }
Snippet 8: Direttive del preprocessore in C
 from pet_sdk import Cat, Dog, get_pet pet = get_pet() if isinstance(pet, Cat): pet.clean_litterbox() elif isinstance(pet, Dog): pet.walk() else: print(f"Don't know how to help a pet of type {type(pet)}")
Snippet 9: Riflessione in Python
 import com.example.coordinates.*; interface Vehicle { public String getName(); public void move(double xCoord, double yCoord); } public class VehicleDriver<T extends Vehicle> { // This class is valid for any other class T which implements // the Vehicle interface private final T vehicle; public VehicleDriver(T vehicle) { System.out.println("VehicleDriver: " + vehicle.getName()); this.vehicle = vehicle; } public void goHome() { this.vehicle.move(HOME_X, HOME_Y); } public void goToStore() { this.vehicle.move(STORE_X, STORE_Y); } }
Snippet 10: Generics in Java
 macro_rules! print_and_return_if_true { ($val_to_check: ident, $val_to_return: expr) => { if ($val_to_check) { println!("Val was true, returning {}", $val_to_return); return $val_to_return; } } } // The following is the same as if for each of x, y, and z, // we wrote if x { println!...} fn example(x: bool, y: bool, z: bool) -> i32 { print_and_return_if_true!(x, 1); print_and_return_if_true!(z, 2); print_and_return_if_true!(y, 3); }
Snippet 11: Macro in Rust

Le macro (come quella in Snippet 11) stanno diventando di nuovo popolari in una nuova generazione di linguaggi di programmazione. Per svilupparli con successo, dobbiamo considerare un argomento chiave: l'igiene.

Macro igieniche e antigieniche

Cosa significa che il codice è "igienico" o "non igienico"? Per chiarire, diamo un'occhiata a una macro Rust, istanziata dalle macro_rules! funzione. Come suggerisce il nome, macro_rules! genera codice in base a regole che definiamo. In questo caso, abbiamo chiamato la nostra macro my_macro e la regola è "Crea la riga di codice let x = $n ", dove n è il nostro input:

 macro_rules! my_macro { ($n) => { let x = $n; } } fn main() { let x = 5; my_macro!(3); println!("{}", x); }
Snippet 12: Igiene nella ruggine

Quando espandiamo la nostra macro (eseguendo una macro per sostituire la sua chiamata con il codice che genera), ci aspetteremmo di ottenere quanto segue:

 fn main() { let x = 5; let x = 3; // This is what my_macro!(3) expanded into println!("{}", x); }
Snippet 13: Il nostro esempio, ampliato

Apparentemente, la nostra macro ha ridefinito la variabile x in modo che sia uguale a 3, quindi possiamo ragionevolmente aspettarci che il programma stampi 3 . Infatti ne stampa 5 ! Sorpreso? In Rust, macro_rules! è igienico rispetto agli identificatori, quindi non "cattura" gli identificatori al di fuori del suo ambito. In questo caso, l'identificatore era x . Se fosse stato catturato dalla macro, sarebbe stato pari a 3.

igiene (sostantivo)

Una proprietà che garantisce che l'espansione di una macro non acquisirà identificatori o altri stati al di fuori dell'ambito della macro. Le macro ei sistemi macro che non forniscono questa proprietà sono detti antigienici .

L'igiene nelle macro è un argomento alquanto controverso tra gli sviluppatori. I sostenitori insistono sul fatto che senza igiene, è fin troppo facile modificare sottilmente il comportamento del codice per sbaglio. Immagina una macro significativamente più complessa dello Snippet 13 utilizzato nel codice complesso con molte variabili e altri identificatori. E se quella macro utilizzasse una delle stesse variabili del tuo codice e non te ne sei accorto?

Non è insolito che uno sviluppatore utilizzi una macro da una libreria esterna senza aver letto il codice sorgente. Ciò è particolarmente comune nei linguaggi più recenti che offrono supporto per le macro (ad es. Rust e Julia):

 #define EVIL_MACRO website="https://evil.com"; int main() { char *website = "https://good.com"; EVIL_MACRO send_all_my_bank_data_to(website); return 1; }
Snippet 14: Una macro C malvagia

Questa macro non igienica in C acquisisce il website dell'identificatore e ne modifica il valore. Naturalmente, l'acquisizione dell'identificatore non è dannosa. È semplicemente una conseguenza accidentale dell'utilizzo delle macro.

Quindi, le macro igieniche sono buone e le macro non igieniche sono cattive, giusto? Sfortunatamente, non è così semplice. C'è una forte argomentazione che le macro igieniche ci limitano. A volte, l'acquisizione dell'identificatore è utile. Rivisitiamo lo Snippet 2, in cui utilizziamo pet_sdk per fornire servizi per tre tipi di animali domestici. Il nostro codice originale è iniziato in questo modo:

 birds = pet_sdk.get_birds() cats = pet_sdk.get_cats() dogs = pet_sdk.get_dogs() for cat in cats: # Cat specific code for dog in dogs: # Dog specific code # etc…
Snippet 15: Back to the Vet—Richiamo del pet sdk

Ricorderai che Snippet 3 era un tentativo di condensare la logica ripetitiva di Snippet 2 in un ciclo all-inclusive. Ma cosa succede se il nostro codice dipende dagli identificatori cats e dogs e volessimo scrivere qualcosa di simile al seguente:

 {animal}s = pet_sdk.get{animal}s() for {animal} in {animal}s: # {animal} specific code
Snippet 16: Cattura identificatore utile (nell'immaginario "TurboPython")

Lo snippet 16 è un po' semplice, ovviamente, ma immagina un caso in cui vorremmo che una macro scrivesse il 100% di una determinata porzione di codice. Le macro igieniche potrebbero essere limitanti in questo caso.

Anche se il dibattito tra igiene e anti-igiene può essere complesso, la buona notizia è che non è necessario prendere posizione. La lingua che stai utilizzando determina se le tue macro saranno igieniche o non igieniche, quindi tienilo a mente quando usi le macro.

Macro moderne

Le macro stanno attraversando un po' di tempo ora. Per molto tempo, l'attenzione dei moderni linguaggi di programmazione imperativi si è spostata dalle macro come parte fondamentale della loro funzionalità, evitandole a favore di altri tipi di metaprogrammazione.

I linguaggi che i nuovi programmatori venivano insegnati nelle scuole (ad es. Python e Java) dicevano loro che tutto ciò di cui avevano bisogno era riflessione e generici.

Nel corso del tempo, quando questi linguaggi moderni sono diventati popolari, le macro sono state associate alla sintassi del preprocessore C e C++ intimidatoria, se i programmatori ne erano a conoscenza.

Con l'avvento di Rust e Julia, però, il trend è tornato ai macro. Rust e Julia sono due linguaggi moderni, accessibili e ampiamente utilizzati che hanno ridefinito e reso popolare il concetto di macro con alcune idee nuove e innovative. Ciò è particolarmente eccitante in Julia, che sembra pronta a prendere il posto di Python e R come linguaggio versatile e facile da usare "batterie incluse".

Quando abbiamo guardato per la prima volta pet_sdk attraverso i nostri occhiali "TurboPython", quello che volevamo davvero era qualcosa come Julia. Riscriviamo Snippet 2 in Julia, usando la sua omoiconicità e alcuni degli altri strumenti di metaprogrammazione che offre:

 using pet_sdk for (pet, care_fn) = (("cat", :clean_litterbox), ("dog", :walk_dog), ("dog", :clean_cage)) get_pets_fn = Meta.parse("pet_sdk.get_${pet}s") @eval begin local animals = $get_pets_fn() #pet_sdk.get_cats(), pet_sdk.get_dogs(), etc. for animal in animals animal.$care_fn # animal.clean_litterbox(), animal.walk_dog(), etc. end end end
Snippet 17: Il potere delle macro di Julia: fare in pet_sdk per noi

Analizziamo lo Snippet 17:

  1. Iteriamo attraverso tre tuple. Il primo di questi è ("cat", :clean_litterbox) , quindi la variabile pet viene assegnata a "cat" e la variabile care_fn viene assegnata al simbolo citato :clean_litterbox .
  2. Usiamo la funzione Meta.parse per convertire una stringa in un Expression , così possiamo valutarla come codice. In questo caso, vogliamo usare il potere dell'interpolazione di stringhe, in cui possiamo inserire una stringa in un'altra, per definire quale funzione chiamare.
  3. Usiamo la funzione eval per eseguire il codice che stiamo generando. @eval begin… end è un altro modo di scrivere eval(...) per evitare di riscrivere il codice. All'interno del blocco @eval c'è il codice che stiamo generando in modo dinamico e in esecuzione.

Il sistema di metaprogrammazione di Julia ci libera davvero di esprimere ciò che vogliamo nel modo in cui lo vogliamo. Avremmo potuto usare molti altri approcci, inclusa la riflessione (come Python in Snippet 5). Avremmo anche potuto scrivere una funzione macro che genera esplicitamente il codice per un animale specifico, oppure avremmo potuto generare l'intero codice come una stringa e utilizzare Meta.parse o qualsiasi combinazione di questi metodi.

Oltre Julia: altri moderni sistemi di metaprogrammazione

Julia è forse uno degli esempi più interessanti e convincenti di un moderno sistema macro, ma non è, in alcun modo, l'unico. Anche Rust è stato determinante nel portare ancora una volta le macro davanti ai programmatori.

In Rust, le macro sono presenti in modo molto più centrale rispetto a Julia, anche se non lo esploreremo completamente qui. Per una serie di ragioni, non puoi scrivere Rust idiomatico senza usare le macro. In Julia, invece, potresti scegliere di ignorare completamente l'omoiconicità e il sistema macro.

Come diretta conseguenza di tale centralità, l'ecosistema Rust ha davvero abbracciato le macro. I membri della comunità hanno creato librerie, prove di concetto e funzionalità incredibilmente interessanti con macro, inclusi strumenti in grado di serializzare e deserializzare i dati, generare automaticamente SQL o persino convertire le annotazioni lasciate nel codice in un altro linguaggio di programmazione, il tutto generato nel codice in tempo di compilazione.

Mentre la metaprogrammazione di Julia potrebbe essere più espressiva e libera, Rust è probabilmente il miglior esempio di un linguaggio moderno che eleva la metaprogrammazione, poiché è ampiamente presente in tutto il linguaggio.

Uno sguardo al futuro

Ora è un momento incredibile per interessarsi ai linguaggi di programmazione. Oggi posso scrivere un'applicazione in C++ ed eseguirla in un browser Web o scrivere un'applicazione in JavaScript da eseguire su un desktop o un telefono. Le barriere all'ingresso non sono mai state così basse e i nuovi programmatori hanno le informazioni a portata di mano come mai prima d'ora.

In questo mondo di scelta e libertà del programmatore, abbiamo sempre più il privilegio di utilizzare linguaggi ricchi e moderni, che raccolgono caratteristiche e concetti dalla storia dell'informatica e dai precedenti linguaggi di programmazione. È emozionante vedere le macro raccolte e rispolverate in questa ondata di sviluppo. Non vedo l'ora di vedere cosa faranno gli sviluppatori di una nuova generazione quando Rust e Julia li introdurranno alle macro. Ricorda, "codice come dati" è più di un semplice slogan. È un'ideologia fondamentale da tenere a mente quando si discute di metaprogrammazione in qualsiasi comunità online o ambiente accademico.

"Codice come dati" è più di un semplice slogan.

Twitta

I 64 anni di storia di Metaprogramming sono stati parte integrante dello sviluppo della programmazione come la conosciamo oggi. Sebbene le innovazioni e la storia che abbiamo esplorato siano solo un angolo della saga della metaprogrammazione, illustrano il robusto potere e l'utilità della moderna metaprogrammazione.