Wann man ein Makro benutzt oder nicht benutzt [closed]
12
Wann sollte ich Makro in meinem Programm verwenden oder nicht?
Diese Frage basiert auf einer informativen Antwort von @tarsius. Ich glaube, viele haben großartige Erfahrungen, um andere zu teilen. Ich hoffe, diese Frage wird zu einer der großen subjektiven Fragen und hilft Emacs-Programmierern.
Verwenden Sie Makros für die Syntax , jedoch niemals für die Semantik.
Es ist in Ordnung, ein Makro für syntaktischen Zucker zu verwenden, aber es ist eine schlechte Idee, Logik in einen Makrokörper einzufügen, unabhängig davon, ob es sich um das Makro selbst oder um die Erweiterung handelt. Wenn Sie das für nötig halten, schreiben Sie stattdessen eine Funktion und ein "Begleitmakro" für den syntaktischen Zucker. Einfach gesagt, wenn Sie ein letin Ihrem Makro haben, suchen Sie nach Ärger.
Warum?
Makros haben eine Menge Nachteile:
Sie werden beim Kompilieren erweitert und müssen daher sorgfältig geschrieben werden, um die Binärkompatibilität über mehrere Versionen hinweg zu gewährleisten . Wenn Sie beispielsweise eine Funktion oder Variable entfernen, die in der Erweiterung eines Makros verwendet wird, wird der Bytecode aller abhängigen Pakete, die dieses Makro verwendeten, beschädigt. Mit anderen Worten, wenn Sie die Implementierung eines Makros inkompatibel ändern, müssen alle abhängigen Pakete neu kompiliert werden. Und binäre Kompatibilität ist nicht immer offensichtlich ...
Sie müssen darauf achten, eine intuitive Auswertungsreihenfolge einzuhalten . Es ist alles zu einfach, ein Makro zu schreiben, das ein Formular zu oft, zur falschen Zeit oder an der falschen Stelle usw. auswertet.
Sie müssen auf die Bindungssemantik achten : Makros erben ihren Bindungskontext von der Erweiterungssite. Dies bedeutet, dass Sie a priori nicht wissen, welche lexikalischen Variablen vorhanden sind und ob die lexikalische Bindung überhaupt verfügbar ist. Ein Makro muss mit beiden Bindungssemantiken arbeiten.
Im Zusammenhang damit müssen Sie beim Schreiben von Makros, die Abschlüsse erzeugen, aufpassen . Denken Sie daran, dass Sie die Bindungen von der Erweiterungssite erhalten, nicht von der lexikalischen Definition. Achten Sie also darauf, was Sie schließen und wie Sie den Abschluss erstellen (denken Sie daran, dass Sie nicht wissen, ob die lexikalische Bindung auf der Erweiterungssite aktiv ist und ob Sie haben sogar Verschlüsse zur Verfügung).
Besonders interessant sind die Hinweise zur lexikalischen Bindung im Vergleich zur Makroexpansion. danke lunaryorn. (Ich habe die lexikalische Bindung noch nie für einen von mir geschriebenen Code aktiviert. Es handelt sich also nicht um einen Konflikt, über den ich jemals nachgedacht habe.)
phils
6
Aufgrund meiner Erfahrungen beim Lesen, Debuggen und Ändern des Elisp-Codes von mir und anderen würde ich vorschlagen, Makros so weit wie möglich zu vermeiden.
Das Offensichtliche an Makros ist, dass sie ihre Argumente nicht bewerten. Dies bedeutet, dass Sie bei einem komplexen Ausdruck in einem Makro keine Ahnung haben, ob er ausgewertet wird oder nicht, und in welchem Kontext, es sei denn, Sie schlagen die Makrodefinition nach. Dies ist möglicherweise keine große Sache für Sie, da Sie die Definition Ihres eigenen Makros kennen (derzeit wahrscheinlich nicht einige Jahre später).
Dieser Nachteil gilt nicht für Funktionen: Alle Argumente werden vor dem Funktionsaufruf ausgewertet. Das heißt, Sie können die Bewertungskomplexität in einer unbekannten Debugsituation aufbauen. Sie können die Definitionen der Funktionen, die Ihnen unbekannt sind, sogar direkt überspringen, solange sie unkomplizierte Daten zurückgeben und Sie davon ausgehen, dass sie den globalen Status nicht berühren.
Um es anders zu formulieren, werden Makros zum Teil der Sprache. Wenn Sie einen Code mit unbekannten Makros lesen, lesen Sie fast einen Code in einer Sprache, die Sie nicht kennen.
Ausnahmen zu den obigen Vorbehalten:
Die Standard - Makros wie push, pop, dolisttun wirklich viel für Sie, und sind zu anderen Programmierern bekannt. Sie sind im Grunde Teil der Sprachspezifikation. Es ist jedoch praktisch unmöglich, ein eigenes Makro zu standardisieren. Es ist nicht nur schwierig, etwas Einfaches und Nützliches wie die obigen Beispiele zu finden, sondern es ist noch schwieriger, jeden Programmierer davon zu überzeugen, es zu lernen.
Außerdem macht es mir nichts aus, dass Makros wie save-selected-windowdiese den Status speichern und wiederherstellen und ihre Argumente auf dieselbe Weise auswerten progn. Ziemlich einfach, macht eigentlich Code einfacher.
Ein weiteres Beispiel für OK-Makros können Sachen wie defhydraoder sein use-package. Diese Makros haben grundsätzlich eine eigene Minisprache. Und sie behandeln immer noch entweder einfache Daten oder verhalten sich so progn. Außerdem befinden sie sich normalerweise auf der obersten Ebene, sodass keine Variablen auf dem Aufrufstapel vorhanden sind, von denen die Makroargumente abhängen können, wodurch es ein bisschen einfacher wird.
Ein Beispiel für ein schlechtes Makro ist meiner Meinung nach magit-section-actionund magit-section-casein Magit. Der erste wurde bereits entfernt, der zweite bleibt jedoch bestehen.
Ich werde stumpf sein: Ich verstehe nicht, was "Verwendung für die Syntax, nicht für die Semantik" bedeutet. Aus diesem Grund wird ein Teil dieser Antwort die Antwort von Lunaryon behandeln , und der andere Teil wird mein Versuch sein, die ursprüngliche Frage zu beantworten.
Wenn das Wort "Semantik" in der Programmierung verwendet wird, bezieht es sich auf die Art und Weise, wie der Autor der Sprache ihre Sprache interpretiert. Dies läuft in der Regel auf eine Reihe von Formeln hinaus, die Grammatikregeln der Sprache in andere Regeln umwandeln, mit denen der Leser vermutlich vertraut ist. Zum Beispiel verwendet Plotkin in seinem Buch Structural Approach to Operational Semantics eine logische Ableitungssprache, um zu erklären, zu welcher abstrakten Syntax die Auswertung führt (was bedeutet es, ein Programm auszuführen). Mit anderen Worten, wenn Syntax auf einer bestimmten Ebene die Programmiersprache ausmacht, muss sie eine gewisse Semantik haben, andernfalls ist sie keine Programmiersprache.
Aber lassen Sie uns für den Moment die formalen Definitionen vergessen, schließlich ist es wichtig, die Absicht zu verstehen. Es scheint also, als würde Lunaryon seine Leser dazu ermutigen, Makros zu verwenden, wenn dem Programm keine neue Bedeutung, sondern nur eine Abkürzung hinzugefügt werden soll. Für mich klingt das merkwürdig und steht in krassem Gegensatz zu der Art und Weise, wie Makros tatsächlich verwendet werden. Nachfolgend einige Beispiele, die eindeutig neue Bedeutungen schaffen:
defun und Freunde, die ein wesentlicher Bestandteil der Sprache sind, können nicht mit den gleichen Begriffen beschrieben werden, mit denen Sie Funktionsaufrufe beschreiben würden.
setfDie Regeln, die die Funktionsweise von Funktionen beschreiben, reichen nicht aus, um die Auswirkungen eines Ausdrucks wie diesen zu beschreiben (setf (aref x y) z).
with-output-to-stringändert auch die Semantik des Codes innerhalb des Makros. Ähnliches with-current-bufferund ein paar andere with-Makros.
dotimes und ähnliches kann nicht mit Funktionen beschrieben werden.
ignore-errors Ändert die Semantik des umschlossenen Codes.
In cl-libpackage, eieiopackage und einigen anderen fast allgegenwärtigen Bibliotheken, die in Emacs Lisp verwendet werden, sind eine ganze Reihe von Makros definiert , die wie Funktionsformen aussehen und unterschiedliche Interpretationen haben können.
Makros sind also eher das Werkzeug, um eine neue Semantik in eine Sprache einzuführen. Ich kann mir keinen alternativen Weg vorstellen, das zu tun (zumindest nicht in Emacs Lisp).
Wenn ich keine Makros verwenden würde:
Wenn sie keine neuen Konstrukte einführen, mit neuer Semantik (dh die Funktion würde den Job genauso gut machen).
Wenn dies nur eine Art Annehmlichkeit ist (z. B. das Erstellen eines Makros mit dem Namen, mvbdas cl-multiple-value-bindnur zur Verkürzung erweitert wird).
Wenn ich erwarte, dass viele Fehlerbehandlungscodes vom Makro ausgeblendet werden (wie bereits erwähnt, ist das Debuggen von Makros schwierig).
Wenn ich Makros gegenüber Funktionen stark bevorzugen würde:
Wenn domänenspezifische Konzepte durch Funktionsaufrufe verdeckt werden. Wenn man zum Beispiel dasselbe contextArgument an eine Reihe von Funktionen übergeben muss, die nacheinander aufgerufen werden müssen, würde ich es vorziehen, sie in ein Makro zu packen, in dem das contextArgument verborgen ist.
Wenn generischer Code in Funktionsform übermäßig ausführlich ist (nackte Iterationskonstrukte in Emacs Lisp sind nach meinem Geschmack zu ausführlich und setzen den Programmierer unnötigerweise den Details der Implementierung auf niedriger Ebene aus).
Illustration
Nachfolgend finden Sie die verwässerte Version, die eine Vorstellung davon vermitteln soll, was unter Semantik in der Informatik zu verstehen ist.
Angenommen, Sie haben eine extrem einfache Programmiersprache mit nur diesen Syntaxregeln:
Jetzt können wir tatsächlich mit dieser Sprache rechnen. Das heißt, wir können die syntaktische Repräsentation nehmen, abstrakt verstehen (dh in abstrakte Syntax umwandeln) und dann semantische Regeln verwenden, um diese Repräsentation zu manipulieren.
Wenn wir über die Lisp-Sprachfamilie sprechen, sind die grundlegenden semantischen Regeln der Funktionsbewertung ungefähr die des Lambda-Kalküls:
(f x) (lambda x y) x
-----, ------------, ---
fx ^x.y x
Zitier- und Makro-Erweiterungsmechanismen werden auch als Meta-Programmier-Tools bezeichnet, dh sie ermöglichen es, über das Programm zu sprechen , anstatt nur zu programmieren. Sie erreichen dies, indem sie mithilfe der "Meta-Ebene" eine neue Semantik erstellen. Das Beispiel eines solchen einfachen Makros ist:
(a . b)
-------
(cons a b)
Dies ist eigentlich kein Makro in Emacs Lisp, aber es hätte sein können. Ich habe mich nur der Einfachheit halber entschieden. Beachten Sie, dass keine der oben definierten semantischen Regeln für diesen Fall gilt, da der nächste Kandidat als Funktion (f x)interpretiert f, obwohl dies anicht unbedingt eine Funktion ist.
"Syntaktischer Zucker" ist in der Informatik eigentlich kein Begriff. Ingenieure meinen damit verschiedene Dinge. Ich habe also keine endgültige Antwort. Da es sich also um eine Semantik handelt, (x y)lautet die Regel für die Interpretation der Syntax des Formulars ungefähr "evaluiere y und rufe dann x mit dem Ergebnis der vorherigen Evaluierung auf". Beim Schreiben (defun x y)wird y nicht ausgewertet und x auch nicht. Ob Sie einen Code mit gleichem Effekt mit unterschiedlicher Semantik schreiben können, bestreitet nicht, dass dieser Code eine eigene Semantik hat.
wvxvw
1
Zu Ihrem zweiten Kommentar: Können Sie mich auf die Definition des verwendeten Makros verweisen, in der steht, was Sie behaupten? Ich habe Ihnen nur ein Beispiel gegeben, in dem eine neue semantische Regel mithilfe eines Makros eingeführt wird. Zumindest im genauen Sinne von CS. Ich denke nicht, dass es produktiv ist, über Definitionen zu streiten, und ich denke auch, dass die in CS verwendeten Definitionen am besten zu dieser Diskussion passen.
wvxvw
1
Nun ... du hast zufällig eine eigene Vorstellung davon, was das Wort "Semantik" bedeutet, was irgendwie ironisch ist, wenn du darüber nachdenkst :) Aber wirklich, diese Dinge haben sehr genaue Definitionen, na ja ... ignoriere diese. Das Buch, das ich in der Antwort verlinkt habe, ist ein Standardlehrbuch über Semantik, wie es im entsprechenden CS-Kurs gelehrt wird. Wenn Sie nur den entsprechenden Artikel des Wikis lesen: en.wikipedia.org/wiki/Semantics_%28computer_science%29, werden Sie sehen, worum es eigentlich geht. Das Beispiel ist die Regel für die Interpretation (defun x y)gegen Funktionsanwendung.
wvxvw
Na ja, ich glaube nicht, dass dies meine Art ist, eine Diskussion zu führen. Es war ein Fehler, einmal zu versuchen, einen zu starten; Ich habe meine Kommentare entfernt, weil das alles keinen Sinn macht. Es tut mir leid, dass ich deine Zeit verschwendet habe.
Aufgrund meiner Erfahrungen beim Lesen, Debuggen und Ändern des Elisp-Codes von mir und anderen würde ich vorschlagen, Makros so weit wie möglich zu vermeiden.
Das Offensichtliche an Makros ist, dass sie ihre Argumente nicht bewerten. Dies bedeutet, dass Sie bei einem komplexen Ausdruck in einem Makro keine Ahnung haben, ob er ausgewertet wird oder nicht, und in welchem Kontext, es sei denn, Sie schlagen die Makrodefinition nach. Dies ist möglicherweise keine große Sache für Sie, da Sie die Definition Ihres eigenen Makros kennen (derzeit wahrscheinlich nicht einige Jahre später).
Dieser Nachteil gilt nicht für Funktionen: Alle Argumente werden vor dem Funktionsaufruf ausgewertet. Das heißt, Sie können die Bewertungskomplexität in einer unbekannten Debugsituation aufbauen. Sie können die Definitionen der Funktionen, die Ihnen unbekannt sind, sogar direkt überspringen, solange sie unkomplizierte Daten zurückgeben und Sie davon ausgehen, dass sie den globalen Status nicht berühren.
Um es anders zu formulieren, werden Makros zum Teil der Sprache. Wenn Sie einen Code mit unbekannten Makros lesen, lesen Sie fast einen Code in einer Sprache, die Sie nicht kennen.
Ausnahmen zu den obigen Vorbehalten:
Die Standard - Makros wie
push
,pop
,dolist
tun wirklich viel für Sie, und sind zu anderen Programmierern bekannt. Sie sind im Grunde Teil der Sprachspezifikation. Es ist jedoch praktisch unmöglich, ein eigenes Makro zu standardisieren. Es ist nicht nur schwierig, etwas Einfaches und Nützliches wie die obigen Beispiele zu finden, sondern es ist noch schwieriger, jeden Programmierer davon zu überzeugen, es zu lernen.Außerdem macht es mir nichts aus, dass Makros wie
save-selected-window
diese den Status speichern und wiederherstellen und ihre Argumente auf dieselbe Weise auswertenprogn
. Ziemlich einfach, macht eigentlich Code einfacher.Ein weiteres Beispiel für OK-Makros können Sachen wie
defhydra
oder seinuse-package
. Diese Makros haben grundsätzlich eine eigene Minisprache. Und sie behandeln immer noch entweder einfache Daten oder verhalten sich soprogn
. Außerdem befinden sie sich normalerweise auf der obersten Ebene, sodass keine Variablen auf dem Aufrufstapel vorhanden sind, von denen die Makroargumente abhängen können, wodurch es ein bisschen einfacher wird.Ein Beispiel für ein schlechtes Makro ist meiner Meinung nach
magit-section-action
undmagit-section-case
in Magit. Der erste wurde bereits entfernt, der zweite bleibt jedoch bestehen.quelle
Ich werde stumpf sein: Ich verstehe nicht, was "Verwendung für die Syntax, nicht für die Semantik" bedeutet. Aus diesem Grund wird ein Teil dieser Antwort die Antwort von Lunaryon behandeln , und der andere Teil wird mein Versuch sein, die ursprüngliche Frage zu beantworten.
Wenn das Wort "Semantik" in der Programmierung verwendet wird, bezieht es sich auf die Art und Weise, wie der Autor der Sprache ihre Sprache interpretiert. Dies läuft in der Regel auf eine Reihe von Formeln hinaus, die Grammatikregeln der Sprache in andere Regeln umwandeln, mit denen der Leser vermutlich vertraut ist. Zum Beispiel verwendet Plotkin in seinem Buch Structural Approach to Operational Semantics eine logische Ableitungssprache, um zu erklären, zu welcher abstrakten Syntax die Auswertung führt (was bedeutet es, ein Programm auszuführen). Mit anderen Worten, wenn Syntax auf einer bestimmten Ebene die Programmiersprache ausmacht, muss sie eine gewisse Semantik haben, andernfalls ist sie keine Programmiersprache.
Aber lassen Sie uns für den Moment die formalen Definitionen vergessen, schließlich ist es wichtig, die Absicht zu verstehen. Es scheint also, als würde Lunaryon seine Leser dazu ermutigen, Makros zu verwenden, wenn dem Programm keine neue Bedeutung, sondern nur eine Abkürzung hinzugefügt werden soll. Für mich klingt das merkwürdig und steht in krassem Gegensatz zu der Art und Weise, wie Makros tatsächlich verwendet werden. Nachfolgend einige Beispiele, die eindeutig neue Bedeutungen schaffen:
defun
und Freunde, die ein wesentlicher Bestandteil der Sprache sind, können nicht mit den gleichen Begriffen beschrieben werden, mit denen Sie Funktionsaufrufe beschreiben würden.setf
Die Regeln, die die Funktionsweise von Funktionen beschreiben, reichen nicht aus, um die Auswirkungen eines Ausdrucks wie diesen zu beschreiben(setf (aref x y) z)
.with-output-to-string
ändert auch die Semantik des Codes innerhalb des Makros. Ähnlicheswith-current-buffer
und ein paar anderewith-
Makros.dotimes
und ähnliches kann nicht mit Funktionen beschrieben werden.ignore-errors
Ändert die Semantik des umschlossenen Codes.In
cl-lib
package,eieio
package und einigen anderen fast allgegenwärtigen Bibliotheken, die in Emacs Lisp verwendet werden, sind eine ganze Reihe von Makros definiert , die wie Funktionsformen aussehen und unterschiedliche Interpretationen haben können.Makros sind also eher das Werkzeug, um eine neue Semantik in eine Sprache einzuführen. Ich kann mir keinen alternativen Weg vorstellen, das zu tun (zumindest nicht in Emacs Lisp).
Wenn ich keine Makros verwenden würde:
Wenn sie keine neuen Konstrukte einführen, mit neuer Semantik (dh die Funktion würde den Job genauso gut machen).
Wenn dies nur eine Art Annehmlichkeit ist (z. B. das Erstellen eines Makros mit dem Namen,
mvb
dascl-multiple-value-bind
nur zur Verkürzung erweitert wird).Wenn ich erwarte, dass viele Fehlerbehandlungscodes vom Makro ausgeblendet werden (wie bereits erwähnt, ist das Debuggen von Makros schwierig).
Wenn ich Makros gegenüber Funktionen stark bevorzugen würde:
Wenn domänenspezifische Konzepte durch Funktionsaufrufe verdeckt werden. Wenn man zum Beispiel dasselbe
context
Argument an eine Reihe von Funktionen übergeben muss, die nacheinander aufgerufen werden müssen, würde ich es vorziehen, sie in ein Makro zu packen, in dem dascontext
Argument verborgen ist.Wenn generischer Code in Funktionsform übermäßig ausführlich ist (nackte Iterationskonstrukte in Emacs Lisp sind nach meinem Geschmack zu ausführlich und setzen den Programmierer unnötigerweise den Details der Implementierung auf niedriger Ebene aus).
Illustration
Nachfolgend finden Sie die verwässerte Version, die eine Vorstellung davon vermitteln soll, was unter Semantik in der Informatik zu verstehen ist.
Angenommen, Sie haben eine extrem einfache Programmiersprache mit nur diesen Syntaxregeln:
Mit dieser Sprache können wir "Programme" wie
(1+0)+1
oder0
oder((0+0))
usw. konstruieren .Aber solange wir keine semantischen Regeln angegeben haben, sind diese "Programme" bedeutungslos.
Angenommen, wir statten unsere Sprache jetzt mit den (semantischen) Regeln aus:
Jetzt können wir tatsächlich mit dieser Sprache rechnen. Das heißt, wir können die syntaktische Repräsentation nehmen, abstrakt verstehen (dh in abstrakte Syntax umwandeln) und dann semantische Regeln verwenden, um diese Repräsentation zu manipulieren.
Wenn wir über die Lisp-Sprachfamilie sprechen, sind die grundlegenden semantischen Regeln der Funktionsbewertung ungefähr die des Lambda-Kalküls:
Zitier- und Makro-Erweiterungsmechanismen werden auch als Meta-Programmier-Tools bezeichnet, dh sie ermöglichen es, über das Programm zu sprechen , anstatt nur zu programmieren. Sie erreichen dies, indem sie mithilfe der "Meta-Ebene" eine neue Semantik erstellen. Das Beispiel eines solchen einfachen Makros ist:
Dies ist eigentlich kein Makro in Emacs Lisp, aber es hätte sein können. Ich habe mich nur der Einfachheit halber entschieden. Beachten Sie, dass keine der oben definierten semantischen Regeln für diesen Fall gilt, da der nächste Kandidat als Funktion
(f x)
interpretiertf
, obwohl diesa
nicht unbedingt eine Funktion ist.quelle
(x y)
lautet die Regel für die Interpretation der Syntax des Formulars ungefähr "evaluiere y und rufe dann x mit dem Ergebnis der vorherigen Evaluierung auf". Beim Schreiben(defun x y)
wird y nicht ausgewertet und x auch nicht. Ob Sie einen Code mit gleichem Effekt mit unterschiedlicher Semantik schreiben können, bestreitet nicht, dass dieser Code eine eigene Semantik hat.(defun x y)
gegen Funktionsanwendung.