Inhalt
- Makros sind nur Funktionen
- Helfen Sie nicht freiwillig für ein Peg Leg
- Kann ich Sie dazu zitieren?
- Mit großer Kraft ...
Wenn Sie eine Funktion schreiben und sie groß genug wird, um ungeschickt zu sein, wissen Sie sicher, dass Sie sie in mehrere kleinere Funktionen aufteilen können, von denen jede verständlich ist. Natürlich ist dies in jeder Sprache eine gute Vorgehensweise, aber es ist besonders einfach und leistungsstark in funktionalen Sprachen, in denen Sie sich keine Gedanken über Nebenwirkungen machen müssen: Wenn Sie einen Teil des Codes aufteilen möchten, können Sie dies tun Verschieben Sie diesen Code schmerzlos wörtlich in eine neue Funktion und verwenden Sie als Parameter alle Einheimischen, mit denen er arbeiten soll. Der schwierigste Teil des gesamten Prozesses besteht darin, einen aussagekräftigen Namen für die neue Hilfsfunktion zu finden!
Makros können ähnliche Probleme entwickeln, wenn sie groß genug werden. Natürlich möchten Sie sie auch aufteilen, oder? Schneiden Sie einfach einen Teil des zu großen Makros aus, fügen Sie ihn in ein neues Makro ein, und schon sind Sie fertig.
(defmacro with-magic [magic-fn log-string & body] (let [magic-sym (gensym "magic-")] `(let [~ magic-sym (fn [arg #] (println ~ log-string arg) #) (~ magic-fn arg #))] ~ @ (postwalk-replace {magic-fn magic-sym} body)))) (macroexpand '(with-magic! "OMG macht Magie mit" (let [x 1 ] (! "test")))) (let * [magic-2968 (clojure.core / fn [arg__2958__auto__] (clojure.core / println "OMG macht Magie mit" arg__2958__auto__) (! arg__2958__auto__)) (let [ x 1] (magic-2968 "test")))
Vielleicht ein dummes Makro, aber kein völlig triviales. Es wird nach einem Symbol gesucht und alle Instanzen dieses Symbols durch eine speziell konstruierte Funktion ersetzt, die etwas druckt, bevor die eigentliche Arbeit ausgeführt wird. Nehmen wir also an, wir entscheiden, dass dieses Makro so kompliziert wird, dass wir es aufteilen möchten. Die logische Vorgehensweise scheint darin zu bestehen, ein separates Makro zu erstellen, das diese "magic-xxx" -Funktionen erstellt.
(defmacro make-magic-fn [magic-fn log-string] `(fn [arg #] (println ~ log-string arg #) (~ magic-fn arg #))) (defmacro with-magic [magic-fn log-string & body] (let [magic-sym (gensym "magic-")] `(let [~ magic-sym ~ (make-magic-fn magic-fn log-string)] ~ @ (postwalk-replace { magic-fn magic-sym} body)))) (macroexpand '(with-magic! "OMG macht Magie mit" (let [x 1] (! "test")))) (let * [magic-3340 # Benutzer $ with_magic $ fn__3327 Benutzer $ with_magic $ fn__3327 @ 15bc46>] (let [x 1] (magic-3340 "test")))
Das sieht überhaupt nicht richtig aus! Anstelle des Netten (fn ...) Form, die wir von der Originalversion erhalten haben, erhalten wir diese Brutto # user $ with_magic $ fn__3327> Dies ist definitiv kein Rechtscode, wenn Sie versuchen, das Makro zu verwenden (anstatt es nur so zu erweitern, wie ich es hier getan habe). Aber was ist schief gelaufen? make-magic-fn ist nur ein Ausschnitt aus dem ursprünglichen With-Magic-Makro. Sollten die Ergebnisse also nicht identisch sein?
Makros sind nur Funktionen
Das Problem ist, dass Makros wirklich nur Funktionen mit zwei speziellen Eigenschaften sind:
- Ihre Argumente werden nicht bewertet
- Ihre Rückgabewerte werden direkt erweitert und als Code behandelt
Das ist es! Der Backtick bietet eine nützliche Verknüpfung für den häufigsten Anwendungsfall. Dabei handelt es sich im Grunde genommen um eine Codevorlage, in die Sie die Argumente des Benutzers an den entsprechenden Stellen einfügen. Aber Sie sollten sich daran erinnern, was tatsächlich hinter den Kulissen vor sich geht. Ihr Makro empfängt als Argumente eine Reihe von Symbolen und Listen und gibt eine Liste zurück.
Hier möchten wir, dass die Argumente für unsere Hilfsfunktion ausgewertet werden: Wir möchten nicht, dass die zurückgegebene Funktion die Symbole "magic-fn" oder "log-string" enthält, sondern dass sie die enthält Werte dieser Bindungen. Und wir möchten nicht, dass das Ergebnis von (make-magic-fn) an Ort und Stelle erweitert und als Code interpretiert wird innerhalb unseres MakrosWir wollen nur die von make-magic-fn zurückgegebene Liste nehmen und in die Liste einfügen, die with-magic irgendwann zurückgibt.
Was wir also tatsächlich suchen, ist so etwas wie ein Makro, aber ohne die Eigenschaften 1 und 2 oben. Erraten Sie, was? Das ist nur eine Funktion! Was wir wirklich versuchen zu schreiben, ist eine Funktion, die es einfach macht, Listen zu erstellen, die wie folgt aussehen (fn [...] (...)), und diese Funktion aus unserem Makro heraus zu verwenden. Da Makros zur Kompilierungszeit über die volle Leistung der Sprache verfügen, können wir diese einfach als eine Funktion erstellen, die wir zur Kompilierungszeit aufrufen.
Und in diesem Fall ist das alles! Wenn Sie (defmacro make-magic-fn) durch (defn make-magic-fn) ersetzen, funktioniert alles wie ein Zauber. Sie können die Verknüpfung in der funktionsbasierten Version von make-magic-fn weiterhin verwenden: Es handelt sich nicht um ein magisches Werkzeug, das nur in Makros funktioniert. Es ist eine praktische Abkürzung zum Generieren von Listen, in denen nur ein Teil des Inhalts angegeben ist.
Helfen Sie nicht freiwillig für ein Peg Leg
Verschachtelte Makros sind wie Amputationen: Sie sind sehr schmerzhaft und es gibt normalerweise eine bessere Lösung, aber in seltenen Fällen haben Sie keine Wahl. Also, während der obige Hinweis zum Schreiben eines Helfers Funktionen statt Helfer Makros wird normalerweise ausreichen, um Sie aus Ärger herauszuhalten, die Tricks des Schreibens verschachtelter Makros sind immer noch eine gute Sache zu wissen.
Angenommen, Sie möchten eine Reihe sehr ähnlicher Makros schreiben, z. B. (mit Explosionen), (mit Spezialeffekten) usw. Vielleicht sehen sie alle so aus, aber mit unterschiedlichen Namen und unterschiedlichen dazwischenliegenden Formen.
(defmacro with-explosions [& body] `(do ~ @ (interpose` (println" BOOM ") body)))
Anstatt dieses Makro hunderte Male zu schreiben, entscheiden Sie sich vernünftigerweise dafür, ein Makro zu schreiben, das eine Reihe von (Name, Zwischenschaltung) Paaren verwendet und für jedes ein Makro schreibt. Der Trick besteht darin, die verschachtelten Anführungszeichen, Anführungszeichen und Gensym-Formulare richtig zu machen. Daher zeige ich hier nur die Lösung und diskutiere dann, warum sie so aussieht, wie sie aussieht.
(defmacro build-movie-set [& szenen] (let [name-vals (partition 2 szenen)] `(do ~ @ (für [[name val] name-vals]` (defmacro ~ (symbol (str "with-) "name" s ")) ([~ '& body #]` (do ~ @ (interpose `(println ~~ val) body #))))))))
Kann ich Sie dazu zitieren?
Das sieht definitiv eklig aus. Ich zähle vier Backtick-Formen, die im gesamten Metamakro unterschiedlich unzitiert sind. Die interessantesten Dinge, die Sie hier sehen werden, sind ~~ val und [~ ’& body #].
Die erste davon ist, dass der Kontext, in dem val verwendet wird, zwei Backtick-Ebenen von dem Kontext entfernt ist, in dem es einen Wert hat, sodass wir ihn zweimal aus dem Zitat entfernen müssen.
Die zweite ist vorhanden, weil, wie Sie wissen, der Backtick-Operator-Namespace alle Symbole qualifiziert, die er sieht ... und & ein legales Symbol ist! Backtick wird also versuchen, es durch Benutzer / & oder Filme / & oder so zu ersetzen. Wenn defmacro das sieht, wird es nicht erkennen, dass Sie & Körper wollten. Die Lösung besteht darin, das Syntaxzitat (mit ~) zu verlassen und ein reales, wörtliches Anführungszeichen (mit ’) einzugeben. Das wird deine & unbehelligt lassen.
Es gibt ähnliche Fälle, in denen Sie möglicherweise Zugriff auf ein Gensym-Symbol von einer weniger tiefen Makroebene benötigen: Sie können auf diejenigen mit ~ foo # zugreifen - das heißt, "zitieren Sie foo # nicht in dieser verschachtelten Backtick-Form". Ich möchte den tatsächlichen Wert von foo # ". Möglicherweise müssen Sie auch etwas im erweiterten Kontext zitieren, aber nicht im Erweiterungskontext: das ist ~ foo. Sie können diese Art von Dingen so lange übereinander stapeln, wie Sie möchten. Hier ist ein Beispiel, mit dem Sie eine bessere IDE-Unterstützung für Ihre automatisch generierten Makros bieten können:
(defmacro was auch immer [name & args] `(defn ^ {: arglists’ ~ ’([data])} ~ name ([data #] (mach etwas mit data # und ~ args)))
Mit großer Kraft ...
Mit den Clojure-Makros können Sie neben dem Schreiben von Code, der Dinge automatisiert, auch das Schreiben von Code automatisieren. Es ist eine leistungsstarke Funktion, hat aber viele scharfe Kanten, auf die Sie achten müssen. Überlegen Sie insbesondere zweimal, bevor Sie versuchen, Makros zu verschachteln: Dies ist normalerweise die falsche Antwort. Aber wenn es richtig ist, erleichtern Ihnen die obigen Hinweise hoffentlich den Vorgang.
Dieser Artikel ist genau und nach bestem Wissen des Autors. Der Inhalt dient nur zu Informations- oder Unterhaltungszwecken und ersetzt nicht die persönliche Beratung oder professionelle Beratung in geschäftlichen, finanziellen, rechtlichen oder technischen Angelegenheiten.
© 2011 amalloy