Code schreiben Code: Eine Einführung in die Theorie und Praxis der modernen Metaprogrammierung

Veröffentlicht: 2022-07-22

Immer wenn ich darüber nachdenke, wie man Makros am besten erklärt, erinnere ich mich an ein Python-Programm, das ich geschrieben habe, als ich anfing zu programmieren. Ich konnte es nicht so organisieren, wie ich wollte. Ich musste eine Reihe leicht unterschiedlicher Funktionen aufrufen, und der Code wurde umständlich. Wonach ich suchte – obwohl ich es damals noch nicht wusste – war Metaprogrammierung .

Metaprogrammierung (Substantiv)

Jede Technik, mit der ein Programm Code als Daten behandeln kann.

Wir können ein Beispiel konstruieren, das die gleichen Probleme zeigt, mit denen ich bei meinem Python-Projekt konfrontiert war, indem wir uns vorstellen, dass wir das Back-End einer App für Haustierbesitzer erstellen. Mit den Tools in einer Bibliothek, pet_sdk , schreiben wir Python, um den Tierbesitzern beim Kauf von Katzenfutter zu helfen:

 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)
Schnipsel 1: Katzenfutter bestellen

Nachdem wir bestätigt haben, dass der Code funktioniert, implementieren wir dieselbe Logik für zwei weitere Arten von Haustieren (Vögel und Hunde). Wir fügen auch eine Funktion hinzu, um Tierarzttermine zu buchen:

 # 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)
Schnipsel 2: Katzen-, Hunde- und Vogelfutter bestellen; Tierarzttermin buchen

Es wäre gut, die sich wiederholende Logik von Snippet 2 in einer Schleife zusammenzufassen, also machten wir uns daran, den Code neu zu schreiben. Da jede Funktion anders benannt ist, stellen wir schnell fest, dass wir nicht bestimmen können, welche (z. B. book_bird_appointment , book_cat_appointment ) in unserer Schleife aufgerufen werden soll:

 import pet_sdk all_animals = pet_sdk.get_birds() + pet_sdk.get_cats() + pet_sdk.get_dogs() for animal in all_animals: # What now?
Schnipsel 3: Was nun?

Stellen wir uns eine aufgeladene Version von Python vor, in der wir Programme schreiben können, die automatisch den endgültigen Code generieren, den wir wollen – eines, in dem wir unser Programm flexibel, einfach und flüssig manipulieren können, als wäre es eine Liste, Daten in einer Datei oder irgendetwas anderes andere übliche Datentypen oder Programmeingaben:

 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)
Codeausschnitt 4: TurboPython: Ein imaginäres Programm

Dies ist ein Beispiel für ein Makro , das in Sprachen wie Rust, Julia oder C verfügbar ist, um nur einige zu nennen – aber nicht in Python.

Dieses Szenario ist ein großartiges Beispiel dafür, wie es nützlich sein könnte, ein Programm zu schreiben, das in der Lage ist, seinen eigenen Code zu ändern und zu manipulieren. Genau das ist der Reiz von Makros, und es ist eine von vielen Antworten auf eine größere Frage: Wie können wir ein Programm dazu bringen, seinen eigenen Code zu prüfen, ihn als Daten zu behandeln und dann auf dieser Grundlage zu handeln?

Im Großen und Ganzen fallen alle Techniken, die eine solche Selbstbeobachtung durchführen können, unter den Oberbegriff „Metaprogrammierung“. Metaprogrammierung ist ein reichhaltiges Teilgebiet des Programmiersprachendesigns und lässt sich auf ein wichtiges Konzept zurückführen: Code als Daten.

Reflexion: Zur Verteidigung von Python

Sie könnten darauf hinweisen, dass Python zwar keine Makrounterstützung bietet, aber viele andere Möglichkeiten zum Schreiben dieses Codes bietet. Hier verwenden wir zum Beispiel die Methode isinstance() , um die Klasse zu identifizieren, von der unsere animal -Variable eine Instanz ist, und rufen die entsprechende Funktion auf:

 # 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)
Schnipsel 5: Ein idiomatisches Beispiel

Wir nennen diese Art der Metaprogrammierungsreflexion und kommen später darauf zurück. Der Code von Snippet 5 ist immer noch etwas umständlich, aber für einen Programmierer einfacher zu schreiben als der von Snippet 2, in dem wir die Logik für jedes aufgelistete Tier wiederholt haben.

Herausforderung

Ändern Sie mithilfe der Methode getattr den vorangehenden Code, um die entsprechenden Funktionen order_*_food und book_*_appointment dynamisch aufzurufen. Dies macht den Code wohl weniger lesbar, aber wenn Sie Python gut kennen, sollten Sie darüber nachdenken, wie Sie getattr anstelle der Funktion isinstance verwenden und den Code vereinfachen können.


Homoikonizität: Die Bedeutung von Lisp

Einige Programmiersprachen, wie Lisp, bringen das Konzept der Metaprogrammierung durch Homoikonizität auf eine andere Ebene.

Homoikonizität (Substantiv)

Die Eigenschaft einer Programmiersprache, bei der es keinen Unterschied zwischen Code und den Daten gibt, auf denen ein Programm operiert.

Lisp wurde 1958 entwickelt und ist die älteste homoikonische Sprache und die zweitälteste höhere Programmiersprache. Lisp, das seinen Namen von „LISt Processor“ erhielt, war eine Revolution in der Computertechnik, die die Verwendung und Programmierung von Computern tiefgreifend geprägt hat. Es ist schwer zu übertreiben, wie grundlegend und unverwechselbar Lisp die Programmierung beeinflusst hat.

Emacs ist in Lisp geschrieben, der einzigen schönen Computersprache. Neal Stephenson

Lisp entstand nur ein Jahr nach FORTRAN, in der Ära der Lochkarten und raumfüllenden Militärcomputer. Dennoch verwenden Programmierer Lisp auch heute noch, um neue, moderne Anwendungen zu schreiben. Der Hauptschöpfer von Lisp, John McCarthy, war ein Pionier auf dem Gebiet der KI. Viele Jahre lang war Lisp die Sprache der KI, wobei Forscher die Fähigkeit schätzten, ihren eigenen Code dynamisch umzuschreiben. Die heutige KI-Forschung konzentriert sich eher auf neuronale Netze und komplexe statistische Modelle als auf diese Art von logischem Generierungscode. Die Forschung zur KI mit Lisp – insbesondere die in den 60er und 70er Jahren am MIT und in Stanford durchgeführte Forschung – hat jedoch das Gebiet, wie wir es kennen, geschaffen, und sein massiver Einfluss hält an.

Das Aufkommen von Lisp machte frühe Programmierer zum ersten Mal mit den praktischen Rechenmöglichkeiten von Dingen wie Rekursion, Funktionen höherer Ordnung und verknüpften Listen vertraut. Es demonstrierte auch die Leistungsfähigkeit einer Programmiersprache, die auf den Ideen des Lambda-Kalküls aufbaut.

Diese Ideen lösten eine Explosion im Design von Programmiersprachen aus und, wie Edsger Dijkstra, einer der größten Namen in der Informatik, es ausdrückte, [...] halfen einer Reihe unserer begabtesten Mitmenschen, zuvor unmögliche Gedanken zu denken.“

Dieses Beispiel zeigt ein einfaches Lisp-Programm (und sein Äquivalent in der bekannteren Python-Syntax), das eine Funktion „Fakultät“ definiert, die die Fakultät ihrer Eingabe rekursiv berechnet und diese Funktion mit der Eingabe „7“ aufruft:

Lispeln Python
( 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 ))

Code als Daten

Obwohl es sich um eine der wirkungsvollsten und folgenreichsten Innovationen von Lisp handelt, hat es Homoikonizität im Gegensatz zu Rekursion und vielen anderen Konzepten, für die Lisp Pionierarbeit geleistet hat, nicht in die meisten heutigen Programmiersprachen geschafft.

Die folgende Tabelle vergleicht homoikonische Funktionen, die sowohl in Julia als auch in Lisp Code zurückgeben. Julia ist eine homoikonische Sprache, die in vielerlei Hinsicht den Hochsprachen ähnelt, mit denen Sie vielleicht vertraut sind (z. B. Python, Ruby).

Das Schlüsselstück der Syntax in jedem Beispiel ist sein Anführungszeichen . Julia verwendet ein : (Doppelpunkt), um zu zitieren, während Lisp ein ' (einfaches Anführungszeichen) verwendet:

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

In beiden Beispielen wandelt das Anführungszeichen neben dem Hauptausdruck ( (x + 1) oder (+ x 1) ) diesen aus Code, der direkt ausgewertet worden wäre, in einen abstrakten Ausdruck um, den wir manipulieren können. Die Funktion gibt Code zurück – keine Zeichenkette oder Daten. Wenn wir unsere Funktion aufrufen und print(function_that_returns_code()) schreiben würden, würde Julia den als x+1 gestringten Code ausgeben (und das Äquivalent gilt für Lisp). Umgekehrt würden wir ohne : (oder ' in Lisp) einen Fehler erhalten, dass x nicht definiert wurde.

Kehren wir zu unserem Julia-Beispiel zurück und erweitern es:

 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: Julia-Beispiel erweitert

Die eval -Funktion kann verwendet werden, um den Code auszuführen, den wir an anderer Stelle im Programm generieren. Beachten Sie, dass der ausgedruckte Wert auf der Definition der x -Variablen basiert. Wenn wir versuchten, eval generierten Code in einem Kontext auszuwerten, in dem x nicht definiert war, erhielten wir einen Fehler.

Homoikonizität ist eine leistungsstarke Art der Metaprogrammierung, die in der Lage ist, neuartige und komplexe Programmierparadigmen freizuschalten, in denen sich Programme spontan anpassen und Code generieren können, um domänenspezifischen Problemen oder neu aufgetretenen Datenformaten gerecht zu werden.

Nehmen wir den Fall von WolframAlpha, wo die homoikonische Wolfram Language Code generieren kann, um sich an eine unglaubliche Bandbreite von Problemen anzupassen. Sie können WolframAlpha fragen: „Was ist das BIP von New York City geteilt durch die Bevölkerung von Andorra?“ und bemerkenswerterweise eine logische Antwort erhalten.

Es scheint unwahrscheinlich, dass irgendjemand jemals daran denken würde, diese obskure und sinnlose Berechnung in eine Datenbank aufzunehmen, aber Wolfram verwendet Metaprogrammierung und einen ontologischen Wissensgraphen, um On-the-Fly-Code zu schreiben, um diese Frage zu beantworten.

Es ist wichtig, die Flexibilität und Leistungsfähigkeit zu verstehen, die Lisp und andere homoikonische Sprachen bieten. Bevor wir weiter tauchen, lassen Sie uns einige der Metaprogrammierungsoptionen betrachten, die Ihnen zur Verfügung stehen:

Definition Beispiele Anmerkungen
Homoikonität Ein Sprachmerkmal, bei dem Code „erstklassige“ Daten sind. Da es keine Trennung zwischen Code und Daten gibt, können die beiden austauschbar verwendet werden.
  • Lispeln
  • Prolog
  • Julia
  • Rebol/Rot
  • Wolfram-Sprache
Hier umfasst Lisp andere Sprachen in der Lisp-Familie, wie Scheme, Racket und Clojure.
Makros Eine Anweisung, Funktion oder ein Ausdruck, der Code als Eingabe akzeptiert und Code als Ausgabe zurückgibt.
  • Rusts macro_rules! , Derive und prozedurale Makros
  • Julias @macro -Aufrufe
  • defmacro
  • C ist #define
(Siehe die nächste Anmerkung zu den Makros von C.)
Präprozessordirektiven (oder Precompiler) Ein System, das ein Programm als Eingabe verwendet und basierend auf im Code enthaltenen Anweisungen eine geänderte Version des Programms als Ausgabe zurückgibt.
  • Makros von C
  • Das # Präprozessorsystem von C++
Die Makros von C werden mit dem Präprozessorsystem von C implementiert, aber die beiden sind getrennte Konzepte.

Der wesentliche konzeptionelle Unterschied zwischen C-Makros (in denen wir die Präprozessordirektive #define verwenden) und anderen Formen von C-Präprozessordirektiven (z. B. #if und #ifndef ) besteht darin, dass wir die Makros verwenden, um Code zu generieren, während wir andere Nicht-# #define verwenden Präprozessordirektiven, um anderen Code bedingt zu kompilieren. Die beiden sind in C und einigen anderen Sprachen eng verwandt, aber sie sind unterschiedliche Arten der Metaprogrammierung.
Betrachtung Die Fähigkeit eines Programms, seinen eigenen Code zu untersuchen, zu modifizieren und zu prüfen.
  • Pythons isinstance , getattr , Funktionen
  • Reflect und typeof von JavaScript
  • getDeclaredMethods von Java
  • Die System.Type -Klassenhierarchie von .NET
Die Reflektion kann zur Kompilierzeit oder zur Laufzeit erfolgen.
Generika Die Fähigkeit, Code zu schreiben, der für eine Reihe verschiedener Typen gültig ist oder der in mehreren Kontexten verwendet, aber an einem Ort gespeichert werden kann. Wir können die Kontexte, in denen der Code gültig ist, entweder explizit oder implizit definieren.

Generika im Template-Stil:

  • C++
  • Rost
  • Java

Parametrischer Polymorphismus:

  • Haskell
  • ML
Generische Programmierung ist ein umfassenderes Thema als generische Metaprogrammierung, und die Grenze zwischen beiden ist nicht klar definiert.

Nach Ansicht des Autors zählt ein parametrisches Typsystem nur dann als Metaprogrammierung, wenn es sich um eine statisch typisierte Sprache handelt.
Eine Referenz für Metaprogrammierung

Schauen wir uns einige praktische Beispiele für Homoikonizität, Makros, Präprozessordirektiven, Reflektion und Generika an, die in verschiedenen Programmiersprachen geschrieben wurden:

 # 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)
Schnipsel 7: Homoikonizität 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; }
Codeausschnitt 8: Präprozessordirektiven 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)}")
Schnipsel 9: Reflexion 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); } }
Schnipsel 10: Generika 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); }
Codeschnipsel 11: Makros in Rust

Makros (wie das in Snippet 11) werden in einer neuen Generation von Programmiersprachen wieder populär. Um diese erfolgreich zu entwickeln, müssen wir ein zentrales Thema berücksichtigen: Hygiene.

Hygienische und unhygienische Makros

Was bedeutet es, wenn Code „hygienisch“ oder „unhygienisch“ ist? Schauen wir uns zur Verdeutlichung ein Rust-Makro an, das von den macro_rules! Funktion. Wie der Name schon sagt, macro_rules! generiert Code basierend auf Regeln, die wir definieren. In diesem Fall haben wir unser Makro my_macro , und die Regel lautet „Erstelle die Codezeile let x = $n “, wobei n unsere Eingabe ist:

 macro_rules! my_macro { ($n) => { let x = $n; } } fn main() { let x = 5; my_macro!(3); println!("{}", x); }
Schnipsel 12: Hygiene in Rust

Wenn wir unser Makro erweitern (ein Makro ausführen, um seinen Aufruf durch den von ihm generierten Code zu ersetzen), würden wir Folgendes erwarten:

 fn main() { let x = 5; let x = 3; // This is what my_macro!(3) expanded into println!("{}", x); }
Snippet 13: Unser Beispiel, erweitert

Anscheinend hat unser Makro die Variable x auf 3 umdefiniert, sodass wir vernünftigerweise erwarten können, dass das Programm 3 ausgibt. Tatsächlich druckt es 5 ! Überrascht? In Rust, macro_rules! ist hygienisch in Bezug auf Identifikatoren, so dass es keine Identifikatoren außerhalb seines Geltungsbereichs „erfassen“ würde. In diesem Fall war der Bezeichner x . Wäre es vom Makro erfasst worden, wäre es gleich 3 gewesen.

Hygiene (Substantiv)

Eine Eigenschaft, die garantiert, dass die Erweiterung eines Makros keine Bezeichner oder andere Zustände erfasst, die außerhalb des Bereichs des Makros liegen. Makros und Makrosysteme, die diese Eigenschaft nicht bieten, werden als unhygienisch bezeichnet.

Hygiene in Makros ist ein etwas umstrittenes Thema unter Entwicklern. Befürworter bestehen darauf, dass es ohne Hygiene allzu leicht ist, das Verhalten Ihres Codes versehentlich zu ändern. Stellen Sie sich ein Makro vor, das wesentlich komplexer ist als Snippet 13, das in komplexem Code mit vielen Variablen und anderen Bezeichnern verwendet wird. Was wäre, wenn dieses Makro eine der gleichen Variablen wie Ihr Code verwendet – und Sie es nicht bemerkt hätten?

Es ist nicht ungewöhnlich, dass ein Entwickler ein Makro aus einer externen Bibliothek verwendet, ohne den Quellcode gelesen zu haben. Dies ist besonders häufig in neueren Sprachen, die Makrounterstützung bieten (z. B. Rust und 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; }
Schnipsel 14: Ein böses C-Makro

Dieses unhygienische Makro in C erfasst die Kennung der website und ändert ihren Wert. Natürlich ist die Erfassung von Kennungen nicht böswillig. Es ist lediglich eine zufällige Folge der Verwendung von Makros.

Hygienische Makros sind also gut und unhygienische Makros sind schlecht, oder? Leider ist es nicht so einfach. Es gibt starke Argumente dafür, dass hygienische Makros uns einschränken. Manchmal ist die Erfassung von Kennungen nützlich. Sehen wir uns Snippet 2 noch einmal an, wo wir pet_sdk verwenden, um Dienste für drei Arten von Haustieren bereitzustellen. Unser ursprünglicher Code begann folgendermaßen:

 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…
Schnipsel 15: Zurück zum Tierarzt – Erinnern an das pet sdk

Sie werden sich erinnern, dass Snippet 3 ein Versuch war, die sich wiederholende Logik von Snippet 2 zu einer allumfassenden Schleife zusammenzufassen. Aber was ist, wenn unser Code von den Bezeichnern cats und dogs abhängt und wir so etwas wie das Folgende schreiben wollten:

 {animal}s = pet_sdk.get{animal}s() for {animal} in {animal}s: # {animal} specific code
Snippet 16: Nützliche Identifier-Erfassung (in imaginärem „TurboPython“)

Snippet 16 ist natürlich etwas einfach, aber stellen Sie sich einen Fall vor, in dem wir möchten, dass ein Makro 100 % eines bestimmten Codeabschnitts schreibt. Hygienische Makros könnten in einem solchen Fall einschränkend sein.

Während die Makro-Debatte hygienisch versus unhygienisch komplex sein kann, ist die gute Nachricht, dass es keine ist, in der Sie eine Haltung einnehmen müssen. Die Sprache, die Sie verwenden, bestimmt, ob Ihre Makros hygienisch oder unhygienisch sind, also denken Sie daran, wenn Sie Makros verwenden.

Moderne Makros

Makros haben jetzt einen Moment Zeit. Lange Zeit verlagerte sich der Fokus moderner imperativer Programmiersprachen weg von Makros als Kernbestandteil ihrer Funktionalität und vermied sie zugunsten anderer Arten der Metaprogrammierung.

Die Sprachen, die neuen Programmierern in den Schulen beigebracht wurden (z. B. Python und Java), sagten ihnen, dass alles, was sie brauchten, Reflexion und Generika seien.

Im Laufe der Zeit, als diese modernen Sprachen populär wurden, wurden Makros mit der einschüchternden C- und C++-Präprozessorsyntax in Verbindung gebracht – falls Programmierer sich ihrer überhaupt bewusst waren.

Mit dem Aufkommen von Rust und Julia hat sich der Trend jedoch wieder zu Makros verlagert. Rust und Julia sind zwei moderne, zugängliche und weit verbreitete Sprachen, die das Konzept von Makros mit einigen neuen und innovativen Ideen neu definiert und populär gemacht haben. Dies ist besonders spannend in Julia, die bereit zu sein scheint, Python und R als einfach zu verwendende, vielseitige Sprache mit „Batterien inklusive“ zu ersetzen.

Als wir pet_sdk zum ersten Mal durch unsere „TurboPython“-Brille betrachteten, wollten wir eigentlich so etwas wie Julia. Lassen Sie uns Snippet 2 in Julia umschreiben, indem wir seine Homoikonizität und einige der anderen Metaprogrammierungswerkzeuge verwenden, die es bietet:

 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
Codeausschnitt 17: Die Leistungsfähigkeit von Julias Makros – pet_sdk für uns funktioniert

Lassen Sie uns Snippet 17 aufschlüsseln:

  1. Wir iterieren durch drei Tupel. Die erste davon ist ("cat", :clean_litterbox) , also wird die Variable pet "cat" zugewiesen und die Variable care_fn wird dem Symbol in :clean_litterbox .
  2. Wir verwenden die Funktion Meta.parse , um einen String in einen Expression umzuwandeln, damit wir ihn als Code auswerten können. In diesem Fall möchten wir die Leistung der Zeichenfolgeninterpolation nutzen, bei der wir eine Zeichenfolge in eine andere einfügen können, um zu definieren, welche Funktion aufgerufen werden soll.
  3. Wir verwenden die eval -Funktion, um den Code auszuführen, den wir generieren. @eval begin… end ist eine andere Art, eval(...) zu schreiben, um das erneute Eingeben von Code zu vermeiden. Innerhalb des @eval Blocks befindet sich Code, den wir dynamisch generieren und ausführen.

Julias Metaprogrammierungssystem gibt uns wirklich die Freiheit, auszudrücken, was wir wollen, wie wir es wollen. Wir hätten mehrere andere Ansätze verwenden können, einschließlich Reflektion (wie Python in Snippet 5). Wir hätten auch eine Makrofunktion schreiben können, die explizit den Code für ein bestimmtes Tier generiert, oder wir hätten den gesamten Code als String generieren und Meta.parse oder eine beliebige Kombination dieser Methoden verwenden können.

Jenseits von Julia: Andere moderne Metaprogrammierungssysteme

Julia ist vielleicht eines der interessantesten und überzeugendsten Beispiele für ein modernes Makrosystem, aber keineswegs das einzige. Auch Rust war maßgeblich daran beteiligt, Makros wieder vor die Programmierer zu bringen.

In Rust spielen Makros viel zentraler als in Julia, obwohl wir das hier nicht vollständig untersuchen werden. Aus einer Reihe von Gründen können Sie kein idiomatisches Rust schreiben, ohne Makros zu verwenden. In Julia könnten Sie sich jedoch dafür entscheiden, die Homoikonizität und das Makrosystem vollständig zu ignorieren.

Als direkte Folge dieser Zentralität hat das Rust-Ökosystem Makros wirklich angenommen. Mitglieder der Community haben einige unglaublich coole Bibliotheken, Proofs of Concept und Funktionen mit Makros erstellt, darunter Tools, die Daten serialisieren und deserialisieren, automatisch SQL generieren oder sogar im Code hinterlassene Anmerkungen in eine andere Programmiersprache konvertieren können, die alle im Code unter generiert werden Kompilierzeit.

Während Julias Metaprogrammierung ausdrucksstärker und freier sein mag, ist Rust wahrscheinlich das beste Beispiel für eine moderne Sprache, die Metaprogrammierung erhöht, da sie in der gesamten Sprache stark vertreten ist.

Ein Blick in die Zukunft

Jetzt ist eine unglaubliche Zeit, um sich für Programmiersprachen zu interessieren. Heute kann ich eine Anwendung in C++ schreiben und in einem Webbrowser ausführen oder eine Anwendung in JavaScript schreiben, die auf einem Desktop oder Telefon ausgeführt wird. Die Eintrittsbarrieren waren noch nie so niedrig, und neuen Programmierern stehen Informationen wie nie zuvor zur Verfügung.

In dieser Welt der Programmiererwahl und -freiheit haben wir zunehmend das Privileg, reichhaltige, moderne Sprachen zu verwenden, die Merkmale und Konzepte aus der Geschichte der Informatik und früheren Programmiersprachen herauspicken. Es ist aufregend zu sehen, wie Makros in dieser Welle der Entwicklung aufgegriffen und abgestaubt werden. Ich kann es kaum erwarten zu sehen, was die Entwickler einer neuen Generation tun werden, wenn Rust und Julia sie in Makros einführen. Denken Sie daran, dass „Code as Data“ mehr als nur ein Schlagwort ist. Es ist eine Kernideologie, die man im Hinterkopf behalten sollte, wenn man in einer Online-Community oder einem akademischen Umfeld über Metaprogrammierung diskutiert.

„Code as Data“ ist mehr als nur ein Schlagwort.

Twittern

Die 64-jährige Geschichte der Metaprogrammierung war ein wesentlicher Bestandteil der Entwicklung der Programmierung, wie wir sie heute kennen. Während die Innovationen und die Geschichte, die wir untersucht haben, nur ein Teil der Metaprogrammierungs-Saga sind, veranschaulichen sie die robuste Kraft und Nützlichkeit der modernen Metaprogrammierung.