lisp-in-rs-macros ist ein einfacher Lisp-Interpreter mit lexikalischem Scope, der ausschließlich mit deklarativen Makros in Rust arbeitet; das Makro lisp! wertet Code zur Compile-Zeit aus und erzeugt einen in einen String umgewandelten Lisp-Wert
lisp!(CAR (CONS (QUOTE A) (QUOTE (B)))) wird während des Makro-Erweiterungsprozesses von rustc berechnet und zu dem String "A" expandiert; die gesamte Implementierung umfasst weniger als 250 Zeilen
- Die Beispiele verwenden
CAR, LIST, QUOTE, PROGN, DEFINE, LAMBDA, DISPLAY, und das Quine-Beispiel zeigt eine Form, in der Lisp-Code zu sich selbst ausgewertet wird
- Explizite Rekursion wird derzeit nicht unterstützt, aber mit self application lassen sich rekursive Abläufe wie das Anhängen von Listen schreiben;
DEFINE selbst verarbeitet jedoch keine rekursiven Definitionen
- Das Beispiel eines metazirkulären Interpreters scheint zu funktionieren, ist aber extrem ineffizient: Die Auswertung von
((lambda (X) X) (quote a)) dauert über 30 Sekunden und erzeugt mehr als eine Million Tokens, sodass cargo per sigkill beendet wird
Lisp, das in Rust-Makros läuft
lisp-in-rs-macros ist ein Lisp-Interpreter mit lexikalischem Scope, der vollständig mit deklarativen Makros in Rust geschrieben ist
- Das Makro
lisp! wertet den übergebenen Lisp-Code aus und wandelt anschließend den berechneten Lisp-Wert in einen String um
- Zum Beispiel expandiert
lisp!(CAR (CONS (QUOTE A) (QUOTE (B)))) zu dem String "A"
- Diese Berechnung geschieht nicht zur Laufzeit, sondern durch rustc zur Compile-Zeit bei der Makro-Erweiterung
- Die Implementierung umfasst weniger als 250 Zeilen
Grundlegende Verwendungsbeispiele
- Durch die Kombination von
CAR, LIST und QUOTE kann das erste Element einer Liste geholt werden
let output = lisp!(CAR (LIST (QUOTE A) (QUOTE B) (QUOTE C)));
assert_eq!(output, "A");
- Um mehrere Ausdrücke auszuwerten, wird
PROGN verwendet
PROGN wertet alle Ausdrücke aus und gibt den Wert des letzten Ausdrucks zurück
DISPLAY wertet zuerst sein Argument aus und expandiert dann in der Form println!("{}", stringify!(evaled_argument)), um die Tokens in einen String umzuwandeln und auszugeben
lisp!(PROGN
(DEFINE message (LAMBDA () (QUOTE "hello there")))
(DISPLAY (message))
(DEFINE NOT (LAMBDA (X) (COND (X NIL) (TRUE TRUE))) )
(DISPLAY (NOT NIL))
);
- Das obige Beispiel gibt
"hello there" und "TRUE" aus
Quine, das sich selbst auswertet
- Das Quine-Beispiel zeigt eine Form, in der Lisp-Code zu sich selbst ausgewertet wird
lisp!
((LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s)))
(QUOTE (LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s)))));
- Dieser Code expandiert zu einem
stringify!-Aufruf wie dem folgenden
stringify!(((LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s)))
(QUOTE (LAMBDA (s) (LIST s (LIST (QUOTE QUOTE) s))))));
Rekursion und self application
- Dieses Lisp unterstützt derzeit keine explizite Rekursion
- Auch ohne explizite Rekursion lässt sich mit Lambda allein rekursives Verhalten erzeugen
- Die
append-Funktion im Beispiel erwähnt den Namen append im Rumpf nicht direkt, sondern führt rekursive Aufrufe über Selbstanwendung mit dem Argument self aus
lisp!(PROGN
(DEFINE append
(LAMBDA (self X Y)
(COND
((EQ X NIL) Y)
(TRUE (CONS (CAR X) (self self (CDR X) Y)))
)))
(append append (QUOTE (A B)) (QUOTE (C D)))
)
- Dieser Code erzeugt als Ergebnis
"(A B C D)"
Einschränkungen bei der Verwendung
- Das Makro
lisp! wertet nur einen einzelnen Ausdruck aus
- Mehrere Ausdrücke müssen mit
(PROGN expr1 expr2 expr3) gebündelt werden
- Die leere Liste ist nicht self-evaluating
- Der Wert der leeren Liste kann mit
NIL oder (QUOTE ()) erhalten werden
- Die leere Liste ist das einzige falsy Objekt
- Dotted Lists werden nicht unterstützt
CONS setzt voraus, dass das letzte Argument eine Liste ist
DEFINE kann überall verwendet werden und wird zur leeren Liste ausgewertet, unterstützt aber keine Rekursion
TRUE ist das einzige self-evaluating Atom, das keine Funktion ist
Unterstützte Formen
DEFINE
QUOTE
LAMBDA
LET
PROGN
CAR
CDR
CONS
LIST
EQ
ATOM
APPLY
DEFINE ist eher eine Form ähnlich internen Definitionen in Scheme als eine echte rekursive Definition im Lisp-Stil
Ein in Lisp geschriebener Lisp-Interpreter
- Das Repository enthält ein Beispiel für einen metazirkulären Interpreter, der auf diesem Lisp geschrieben ist
- Das Beispiel definiert einen
Y2-Kombinator für zwei Argumente, CADR, CAAR, ASSOC, eval und mehr
- Der Interpreter scheint zu funktionieren, aber der Versuch,
((lambda (X) X) (quote a)) auszuwerten, dauert über 30 Sekunden
- Diese Auswertung erzeugt mehr als eine Million Tokens und wächst schließlich so stark, dass cargo per sigkill beendet wird
- Rekursion mit einem expliziten Y-Kombinator ist hier besonders ineffizient
- Um das zu beheben, wird vorgeschlagen, ein explizites Rekursions-Primitive hinzuzufügen
- Als Walkthrough zum Schreiben eines metazirkulären Evaluators wird Paul Grahams
"Roots of Lisp" empfohlen
Implementierungsansatz und Referenzen
- Die technische Erklärung steht in
EXPLANATION.md
- Die Makros simulieren im Kern eine SECD machine
- Die SECD machine ist eine einfache stapelbasierte abstrakte Maschine zur Auswertung von Lambda-Kalkül-Termen
Referenzen
Functional Programming: Application and Implementation von Peter Henderson
- Ager, Mads Sig, et al.
"A functional correspondence between evaluators and abstract machines."
The Implementation of Functional Programming Languages von Simon Peyton Jones
- Matt Mights Blogbeiträge zu Lisp: https://matt.might.net
TODO
letrec hinzufügen
- Rekursives
define hinzufügen
1 Kommentare
Hacker-News-Kommentare
Greenspuns zehntes Gesetz schlägt wieder zu: https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule
Erst mit C++26 wird man mit
Args...[0]an dascareines Type-Name-Parameter-Packs kommen können.Ich verstehe nicht, warum man nicht einfach
nilsowiecar/cdr-Funktionen für leere Parameter-Packs einführt und es erlaubt, Parameter-Packs zu speichern, statt des jetzigen syntaktischen Durcheinanders.Ich habe früher einmal etwas Ähnliches gemacht, und dabei gab es das Problem, dass man Symbole mit Bindestrichen nicht definieren konnte.
So etwas wie
DEFINE MY-FN...ging nicht, weil Rust am Bindestrich den Token trennt.Das ist nur ein kleiner Unterschied, aber in der Praxis konnte man echte Lisp-Codefragmente nicht einfach hineinkopieren und musste alles in Unterstriche umwandeln. Ich frage mich, ob diese Implementierung dasselbe Problem hat.
$x:identmatchen kann, und deshalb werden Bindestriche in Atomen nicht unterstützt.Stattdessen müsste man wohl so etwas wie
$x:ident $(- $y:ident)*matchen. Ein paar Details in einigen Makro-Zweigen müsste man ändern, aber es scheint machbar.DEFINE MYᜭFN...funktioniert einwandfrei.Es wäre schön, nicht nur Makros zu haben, sondern eine gut unterstützte Lisp-Implementierung auf Basis von Rust.
Ich frage mich, wie viel Speichersicherheit man behält oder verliert, wenn man sie auf Rust aufsetzt. Ob sich der Borrow Checker auf eine vernünftige Weise überhaupt nutzen ließe?
Lisp wird üblicherweise über seine dynamische Natur definiert, und Laufzeit-Typprüfungen machen einen großen Teil davon aus. Wenn man vom Programmierer verlangt, sich im Voraus Gedanken über die Objektverwaltung zu machen, kollidiert das mit der Freiheit und Ausdrucksstärke, die man von solchen Systemen erwartet.
Dafür kann der Compiler selbst vergleichsweise einfach sein. Gewöhnlicher Code ohne zusätzliche Deklarationen ist standardmäßig sicher, und in Bytecode-VMs wie CLISP oder auf Lisp-Maschinen mit Hardware-Typprüfung könnten solche Deklarationen sogar ignoriert werden und trotzdem immer sicher bleiben.
SBCL kompiliert Code ziemlich schnell, und ich habe gehört, dass andere Implementierungen noch schneller sind. Dagegen dürfte der Rust-Compiler jungen Programmierern eher den Begriff Thrashing näherbringen.
Ich denke, das sind zwei Welten, die weit weniger kompatibel sind, als es auf den ersten Blick scheint. Lisp ist im Kern die Vorzeigesprache der Philosophie „The Right Thing“, und C ist eine Sprache von „Worse is Better“. Rust ist keines von beidem; es wirkt wie etwas völlig anderes, das einen neuen Namen braucht, weil es die schlechten Eigenschaften beider Philosophien widerspiegelt.
Das soll den ursprünglichen Beitrag aber nicht herabsetzen, es ist trotzdem ein cooler Hack.
Es gibt auch andere Lisps (https://github.com/alilleybrinker/langs-in-rust). Sie scheinen allerdings weniger aktiv gepflegt zu werden.
Es hat beim Bauen Spaß gemacht, und ich habe außerdem gelernt, dass rust-analyser keine Makros verarbeiten kann, die mehrere Millionen Tokens erzeugen.
Eigentlich soll hier wohl jeder begeistert „macht Spaß“ rufen, aber immer wenn ich so etwas sehe, gefällt mir nicht, dass Rust so etwas überhaupt möglich macht.
Rust war ohnehin nie eine einfache Sprache, aber inzwischen wirkt sie auf mich deutlich schwerer beherrschbar als am Anfang.
Ich verstehe nur nicht ganz, warum du es schlimm findest, dass so etwas möglich ist. Das Makrosystem kann zwar fast unendlich komplexen Code erzeugen, aber ich weiß nicht, ob eine in Makros eingesperrte Lisp-Implementierung wirklich ein starkes Beispiel dafür ist, dass Rust schwerer wartbar geworden ist als früher.
Andererseits würde ich auch gern ein Lisp sehen, das mit Rusts Typsystem umgesetzt ist, einfach weil dessen Typsystem wie C++-Templates oder Haskells Typsystem Turing-vollständig ist.
Gute Beispiele dafür sind non-lexical lifetimes,
impl Traitan der Rückgabeposition und asynchrone Traits. Vor 1.0 gab es sogar eingebaute GC-Referenzen mit spezieller Syntax, und solche Features wurden auch wieder entfernt.Wenn du eine Sprache willst, deren Prinzip Einfachheit ist, dann war Rust nie so eine Sprache, und es gibt viele andere Optionen.
Ich habe nachgesehen, und ich habe diese Wette gewonnen: https://github.com/kchanqvq/CSP
Vor allem meine ich die Seite des „Schreibens“ von Makros; ich sehe das eher als optionales Zusatzfeature, das man nutzen kann, aber nicht muss.
Wow, das verwendet tatsächlich macro_rules.
Aber hieß es nicht immer, C++ sei keine vernünftige Sprache, weil Templates Turing-vollständig sind?
Ich weiß nicht, zu welcher Kategorie Rusts Makrosystem gehört.
macro_expand, und auch die Rust-Tools sind ziemlich gut gemacht.Carp darf man auch nicht vergessen. Ein Lisp mit Borrow Checker, so etwas wie das „Rust“ der Lisp-Welt.
1: https://github.com/carp-lang/Carp