Wie man eine Sprache homoikonisch macht

16

Laut diesem Artikel gibt die folgende Zeile des Lisp-Codes "Hello world" in der Standardausgabe aus.

(format t "hello, world")

Lisp, eine homoikonische Sprache , kann Code wie folgt als Daten behandeln:

Stellen Sie sich nun vor, wir hätten folgendes Makro geschrieben:

(defmacro backwards (expr) (reverse expr))

rückwärts ist der Name des Makros, das einen Ausdruck (als Liste dargestellt) annimmt und ihn umkehrt. Hier ist wieder "Hallo Welt", diesmal mit dem Makro:

(backwards ("hello, world" t format))

Wenn der Lisp-Compiler diese Codezeile sieht, schaut er auf das erste Atom in der Liste ( backwards) und bemerkt, dass er ein Makro benennt. Die nicht ausgewertete Liste wird ("hello, world" t format)an das Makro übergeben, in das die Liste neu angeordnet wird (format t "hello, world"). Die resultierende Liste ersetzt den Makroausdruck und wird zur Laufzeit ausgewertet. Die Lisp-Umgebung erkennt, dass es sich bei ihrem ersten atom ( format) um eine Funktion handelt, wertet sie aus und übergibt ihr die restlichen Argumente.

In Lisp ist diese Aufgabe einfach (korrigieren Sie mich, wenn ich falsch liege), da Code als Liste ( s-Ausdrücke ?) Implementiert ist .

Schauen Sie sich jetzt dieses OCaml-Snippet an (das keine homoikonische Sprache ist):

let print () =
    let message = "Hello world" in
    print_endline message
;;

Stellen Sie sich vor, Sie möchten OCaml, das im Vergleich zu Lisp eine viel komplexere Syntax verwendet, eine Homoikonizität verleihen. Wie würdest du das machen? Muss die Sprache eine besonders einfache Syntax haben, um Homoikonizität zu erreichen?

EDIT : Von diesem Thema aus habe ich einen anderen Weg gefunden, um Homoikonizität zu erreichen, der sich von dem von Lisp unterscheidet: den in der io-Sprache implementierten . Es kann diese Frage teilweise beantworten.

Beginnen wir hier mit einem einfachen Block:

Io> plus := block(a, b, a + b)
==> method(a, b, 
        a + b
    )
Io> plus call(2, 3)
==> 5

Okay, der Block funktioniert also. Der Plusblock fügte zwei Zahlen hinzu.

Lassen Sie uns nun einen Blick auf diesen kleinen Kerl werfen.

Io> plus argumentNames
==> list("a", "b")
Io> plus code
==> block(a, b, a +(b))
Io> plus message name
==> a
Io> plus message next
==> +(b)
Io> plus message next name
==> +

Heiße heilige kalte Form. Sie können nicht nur die Namen der Blockparameter abrufen. Und Sie können nicht nur eine Zeichenfolge des vollständigen Quellcodes des Blocks abrufen. Sie können sich in den Code einschleichen und die darin enthaltenen Nachrichten durchlaufen. Und das Erstaunlichste von allem: Es ist schrecklich einfach und natürlich. Getreu Ios Suche. Rubys Spiegel kann nichts davon sehen.

Aber, whoa whoa, hey jetzt, rühr das Zifferblatt nicht an.

Io> plus message next setName("-")
==> -(b)
Io> plus
==> method(a, b, 
        a - b
    )
Io> plus call(2, 3)
==> -1
einschließen
quelle
1
Vielleicht möchten Sie einen Blick darauf werfen, wie Scala seine Makros erstellt hat
Bergi
1
@Bergi Scala hat einen neuen Ansatz für Makros: scala.meta .
Martin Berger
Ich habe immer gedacht, Homoikonizität wird überbewertet. In jeder ausreichend mächtigen Sprache können Sie immer eine Baumstruktur definieren, die die Struktur der Sprache selbst widerspiegelt, und Dienstprogrammfunktionen können so geschrieben werden, dass sie nach Bedarf in und aus der Quellsprache (und / oder einem kompilierten Formular) übersetzt werden. Ja, es ist etwas einfacher in LISPs, aber da (a) die überwiegende Mehrheit der Programmierarbeit keine Metaprogrammierung sein sollte und (b) LISP die Sprachklarheit geopfert hat, um dies zu ermöglichen, denke ich nicht, dass sich der Kompromiss lohnt.
Periata Breatta
@PeriataBreatta Sie haben Recht, aber der Hauptvorteil von MP ist, dass MP Abstraktionen ohne Laufzeiteinbußen ermöglicht . So löst MP die Spannung zwischen Abstraktion und Performance, wenn auch auf Kosten der zunehmenden Sprachkomplexität. Lohnt es sich? Ich würde sagen, die Tatsache, dass alle wichtigen PLs MP-Erweiterungen haben, zeigt, dass viele arbeitende Programmierer die Kompromisse, die MP bietet, nützlich finden.
Martin Berger

Antworten:

10

Sie können jede Sprache homoikonisch machen. Im Wesentlichen tun Sie dies, indem Sie die Sprache 'spiegeln' (dh für jeden Sprachkonstruktor fügen Sie eine entsprechende Darstellung dieses Konstruktors als Daten hinzu, denken Sie an AST). Sie müssen auch einige zusätzliche Vorgänge wie das Zitieren und das Aufheben des Zitierens hinzufügen. Das ist mehr oder weniger alles.

Lisp hatte das schon früh aufgrund seiner einfachen Syntax, aber die MetaML-Sprachfamilie von W. Taha zeigte, dass es für jede Sprache möglich ist.

Der gesamte Prozess wird in Modellierung der homogenen generativen Metaprogrammierung beschrieben . Eine leichtere Einführung in dasselbe Material finden Sie hier .

Martin Berger
quelle
1
Korrigiere mich, wenn ich falsch liege. "Spiegeln" hängt mit dem zweiten Teil der Frage zusammen (Homoikonizität in io lang), richtig?
einschließlich
@Ignus Ich bin mir nicht sicher, ob ich Ihre Bemerkung richtig verstanden habe. Der Zweck der Homoikonizität besteht darin, die Behandlung von Code als Daten zu ermöglichen. Das bedeutet, dass jede Form von Code als Daten dargestellt werden muss. Es gibt verschiedene Möglichkeiten, dies zu tun (z. B. ASTs Quasi-Anführungszeichen, bei denen Typen verwendet werden, um Code von Daten zu unterscheiden, wie dies durch den modularen Staging-Ansatz mit geringem Gewicht erfolgt), aber alle erfordern eine Verdoppelung / Spiegelung der Sprachsyntax in irgendeiner Form.
Martin Berger
Ich gehe davon aus, dass @Ignus von einem Blick auf MetaOCaml profitieren würde. Bedeutet "homoikonisch" nur, zitierbar zu sein? Ich gehe davon aus, dass mehrstufige Sprachen wie MetaML und MetaOCaml noch weiter gehen?
Steven Shaw
1
@StevenShaw MetaOCaml ist sehr interessant, insbesondere Olegs neues BER MetaOCaml . Es ist jedoch insofern etwas eingeschränkt, als es nur Laufzeit-Metaprogrammierungen durchführt und Code nur über Quasi-Anführungszeichen darstellt, was nicht so aussagekräftig ist wie ASTs.
Martin Berger
7

Der Ocaml-Compiler ist in Ocaml selbst geschrieben, daher gibt es mit Sicherheit eine Möglichkeit, Ocaml-ASTs in Ocaml zu manipulieren.

Man könnte sich vorstellen ocaml_syntax, der Sprache einen eingebauten Typ hinzuzufügen und eine defmacroeingebaute Funktion zu haben, die beispielsweise eine Eingabe von Typ akzeptiert

f : ocaml_syntax -> ocaml_syntax

Was ist nun die Art von defmacro? Nun, das hängt wirklich von der Eingabe ab, denn selbst wenn fes sich um die Identitätsfunktion handelt, hängt der Typ des resultierenden Codeteils von der übergebenen Syntax ab.

Dieses Problem tritt in lisp nicht auf, da die Sprache dynamisch typisiert wird und dem Makro selbst zur Kompilierungszeit kein Typ zugewiesen werden muss. Eine Lösung wäre zu haben

defmacro : (ocaml_syntax -> ocaml_syntax) -> 'a

Damit kann das Makro in jedem Kontext verwendet werden. Dies ist jedoch nicht sicher, da es die boolVerwendung von a anstelle von a ermöglicht stringund das Programm zur Laufzeit abstürzt.

Die einzige prinzipielle Lösung in einer statisch typisierten Sprache wäre, abhängige Typen zu haben , bei denen der Ergebnistyp von defmacroder Eingabe abhängt. An dieser Stelle wird es jedoch ziemlich kompliziert, und ich möchte Sie zunächst auf die schöne Dissertation von David Raymond Christiansen hinweisen .

Fazit: Komplizierte Syntax ist kein Problem, da es viele Möglichkeiten gibt, die Syntax innerhalb der Sprache darzustellen und möglicherweise Metaprogramme wie eine quoteOperation zu verwenden, um die "einfache" Syntax in die interne einzubetten ocaml_syntax.

Das Problem ist, dass dies gut typisiert ist, insbesondere mit einem Laufzeitmakromechanismus, der keine Tippfehler zulässt.

Ein Mechanismus zur Kompilierung von Makros in einer Sprache wie Ocaml ist natürlich möglich, siehe zB MetaOcaml .

Ebenfalls möglicherweise nützlich: Jane Street über Metaprogrammierung in Ocaml

Cody
quelle
2
MetaOCaml verfügt über eine Laufzeit-Meta-Programmierung, keine Meta-Programmierung zur Kompilierungszeit. Auch das Typisierungssystem von MetaOCaml hat keine abhängigen Typen. (MetaOCaml erwies sich auch als typenwidrig!) Template Haskell hat einen interessanten Ansatz: Jede Phase ist typensicher, aber wenn wir eine neue Phase betreten, müssen wir die Typenkontrolle erneut durchführen. Nach meiner Erfahrung funktioniert dies in der Praxis sehr gut, und Sie verlieren nicht die Vorteile der Typensicherheit in der Endphase (Laufzeit).
Martin Berger
@cody es ist möglich, Metaprogrammierung in OCaml auch mit Extension Points zu haben , oder?
einschließlich
@Ignus Ich fürchte, ich weiß nicht viel über Erweiterungspunkte, obwohl ich im Link zum Jane Street-Blog darauf verweise.
Cody
1
Mein C-Compiler ist in C geschrieben, aber das heißt nicht, dass Sie den AST in C manipulieren können ...
BlueRaja - Danny Pflughoeft
2
@immibis: Offensichtlich, aber wenn es das ist, was er meinte, dann ist diese Aussage sowohl vakuumiert als auch unabhängig von der Frage ...
BlueRaja - Danny Pflughoeft
1

Betrachten Sie als Beispiel F # (basierend auf OCaml). F # ist nicht vollständig homoikonisch, unterstützt jedoch das Abrufen des Codes einer Funktion als AST unter bestimmten Umständen.

In F # wird Ihr printals das dargestellt Expr, das gedruckt wird als:

Let (message, Value ("Hello world"), Call (None, print_endline, [message]))

Um die Struktur besser hervorzuheben, gibt es eine alternative Möglichkeit, wie Sie dieselbe erstellen können Expr:

let messageVar = Var("message", typeof<string>)
let expr = Expr.Let(messageVar,
                    Expr.Value("Hello world"),
                    Expr.Call(print_endline_method, [Expr.Var(messageVar)]))
svick
quelle
Ich hab es nicht verstanden. Sie meinen, dass Sie mit F # den AST eines Ausdrucks "erstellen" und dann ausführen können? Wenn ja, worin besteht der Unterschied zu Sprachen, mit denen Sie die eval(<string>)Funktion verwenden können? ( Vielen Quellen zufolge unterscheidet sich die Eval-Funktion von der Homoikonizität. Ist dies der Grund, warum Sie gesagt haben, dass F # nicht vollständig homoikonisch ist?)
einschließlich
@Ignus Sie können den AST selbst erstellen oder vom Compiler ausführen lassen. Homoiconicity "ermöglicht den Zugriff auf den gesamten Code in der Sprache und die Umwandlung als Daten" . In F # können Sie Zugriff auf einige Code als Daten. (Zum Beispiel müssen Sie printmit dem [<ReflectedDefinition>]Attribut markieren .)
Svick