Heim > Artikel > Backend-Entwicklung > Pretty-Printing ist Zusammenstellung
Wadlers A Prettier Printer ist eine klassische Funktionsperle. Allerdings hatte ich – sei es aufgrund von Haskells Faulheit oder meiner eigenen – Mühe, seine Ideen in anderen Sprachen (oder sogar in Haskell, fünf Minuten nach dem Lesen des Artikels) erneut umzusetzen. Zum Glück erkannte Lindig dieses Problem und brachte mit Strictly Pretty Feuer unter die Massen. Aber selbst das war mir nicht heruntergekommen genug.
Aber nachdem ich noch etwas Zeit damit verbracht habe, an den Ideen aus beiden Papieren herumzubasteln, denke ich, dass ich endlich auf die Idee gekommen bin.
Wie der Titel schon sagt, stellen wir uns Pretty-Printing als einen Prozess der Kompilierung (und Ausführung) von Programmen vor, die in einer abstrakten „Dokumentensprache“ geschrieben sind. Wie die meisten Programmiersprachen ist diese Dokumentsprache – die wir
nennen
Doc – enthält Ausdrücke, die zusammen zusammengestellt werden können; Dies macht es für den Menschen einfacher, darüber nachzudenken. Wir werden Ausdrücke in Doc zu Anweisungen in einer Art Assemblersprache (ASM) kompilieren. Anweisungen in ASM lassen sich viel einfacher in Strings umwandeln.
Hier ist eine schematische Darstellung des Prozesses:
Als konkretes Beispiel nehmen wir an, wir möchten die verschachtelte Liste hübsch drucken:
['onions', ['carrots', 'celery'], 'turnips']
Hier ist ein Doc-Programm dafür:
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
Wir treffen uns in Kürze mit Gruppe, Nest usw. Im Moment reicht es aus, ein allgemeines Gefühl für die Dokumentensprache zu bekommen.
Dieses Programm wird dann (mit bestimmten „Architekturparametern“) in die ASM-Anweisungen kompiliert:
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
die dann als String interpretiert werden:
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Eine oben erwähnte wichtige Funktion ist, dass der Compiler über einen konfigurierbaren „Architekturparameter“ verfügt, bei dem es sich um eine maximale Ziellinienbreite handelt. Abhängig vom Wert dieses Parameters gibt der Compiler unterschiedliche ASM-Anweisungen für dasselbe Doc-Programm aus. Im obigen Beispiel haben wir eine Zielbreite von 30 verwendet. Wenn wir stattdessen 20 verwendet hätten, wären die ausgegebenen Montageanweisungen anders, ebenso wie die resultierende Zeichenfolge:
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
Und wenn wir 60 verwendet hätten, wäre es:
['onions', ['carrots', 'celery'], 'turnips']
Die Assemblersprache ist unkompliziert, daher werden wir sie zuerst in Angriff nehmen. Wir stellen uns ASM-Anweisungen als Steuerung eines wirklich einfachen Druckgeräts vor, das nur zwei Dinge tun kann:
Daher besteht ASM nur aus zwei Anweisungen:
ASM-Programme werden als Strings interpretiert, indem sie ihre Anweisungen nacheinander ausführen. Verfolgen wir als Beispiel die Ausführung des Programms:
['onions', ['carrots', 'celery'], 'turnips']
Wir verwenden ein > um die ausgeführte Anweisung anzugeben und die aktuelle Ausgabe unten anzuzeigen. Das Zeichen ^ gibt die aktuelle Position des „Druckkopfes“ an. Wir verwenden auch _-Zeichen, um Leerzeichen anzuzeigen, da diese sonst schwer zu verfolgen sind.
Die erste TEXT-Anweisung bewirkt, dass die Zeichenfolge „Hallo“ ausgegeben wird:
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
ZEILE 2 geht dann zur nächsten Zeile über und rückt den Kopf um 2 Leerzeichen ein:
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
Dann führt TEXT „indented“ dazu, dass „indented“ hinzugefügt wird:
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Gefolgt von „world“, aufgrund von TEXT „world“:
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
ZEILE 0 rückt den Drucker zur nächsten Zeile vor (und rückt überhaupt nicht ein):
['onions', ['carrots', 'celery'], 'turnips']
Und schließlich gibt TEXT „goodbye“ „goodbye“ aus:
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye'
Wir stellen ASM-Anweisungen als „Summentyp“ dar:
Das heißt:
> TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello ^
Um eine Liste von AsmInsts in die Zeichenfolge zu interpretieren, die sie darstellen, müssen Sie lediglich die Anweisungen durchlaufen und „das Richtige tun“:
TEXT 'hello' > LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __ ^
Bei TEXT-Anweisungen hängt der Interpreter den Text an das Ergebnis an; Bei LINE-Anweisungen fügt der Interpreter eine neue Zeile ('n') gefolgt von Einzugsleerzeichen an.
Wir können die Interpretation mit den ASM-Anweisungen aus dem obigen Beispiel testen, übersetzt in Python:
TEXT 'hello' LINE 2 > TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented ^
Wir mögen ASM, weil es einfach zu interpretieren ist. Aber es ist mühsam zu verwenden. Dies motiviert die menschenfreundlichere Doc-Sprache. Während ASM-Programme Sequenzen von Anweisungen sind, sind Doc-Programme Zusammensetzungen von Ausdrücken. Diese Ausdrücke werden durch die folgende Grammatik zusammengefasst:
TEXT 'hello' LINE 2 TEXT 'indented' > TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
Zum Beispiel:
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' > LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
ist ein Doc-Ausdruck, ebenso wie:
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 > TEXT 'goodbye' == OUTPUT == hello __indented world goodbye ^
Was bedeuten diese?
Also zum Beispiel:
AsmInst = str | int
stellt die Zeichenfolge dar:
def interpret(insts: list[AsmInst]) -> str: """Interpret the ASM instructions as a string.""" result = "" for inst in insts: match inst: case text if isinstance(text, str): result += inst case indent if isinstance(indent, int): result += f"\n{' ' * indent}" return result
Der zweite, komplexere Ausdruck:
['onions', ['carrots', 'celery'], 'turnips']
kann Folgendes darstellen:
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
oder:
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
Abhängig vom Wert des „Architekturparameters“ für die maximale Ziellinienbreite, der dem Compiler bereitgestellt wird. Daher können br-Ausdrücke entweder als Zeilenumbrüche oder als regulärer Text behandelt werden. In diesem Fall wird ihr Textwert verwendet (oder '', wenn kein Textargument angegeben wurde).
Wir stellen Doc-Ausdrücke mithilfe von Strs- und Python-Klassen dar. Insbesondere:
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Was ist mit DocExpr DocExpr? Wir werden diejenigen darstellen, die eine zusätzliche Concat-Klasse verwenden:
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
Wir möchten die Verwendung zum Kombinieren von Ausdrücken unterstützen, daher müssen wir __add__ und __radd__ für jede der Variantenklassen implementieren. Durch das Hinzufügen zweier Doc-Ausdrücke mithilfe von just wird ein Concat der beiden erstellt. Es ist ganz einfach, dies manuell zu tun, z. B.:
['onions', ['carrots', 'celery'], 'turnips']
Aber wir können uns etwas Tipparbeit ersparen, indem wir einen Dekorateur definieren, der das für uns erledigt:
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye'
Die Variantenklassen sehen jetzt so aus:
> TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello ^
Unsere Aufgabe besteht nun darin, einen Compiler zu schreiben, der Ausdrücke in der Doc-Sprache in die entsprechenden ASM-Anweisungen übersetzt, vorausgesetzt, eine maximale Zielzeilenbreite ist gegeben:
TEXT 'hello' > LINE 2 TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __ ^
Es erweist sich jedoch als einfacher, zunächst Doc-Ausdrücke in Ausdrücke in einer Intermediate Representation (IR)-Sprache „herabzustufen“ und die IR-Ausdrücke dann in ASM zu kompilieren. Führt diesen zusätzlichen „Durchlauf“ ein, macht jeden Schritt klarer.
Unser Schaltplan, der den Pretty-Printing-Prozess beschreibt, war also etwas zu stark vereinfacht. Hier ist das vollständige Bild:
IR-Ausdrücke ähneln in vielerlei Hinsicht Doc-Ausdrücken:
TEXT 'hello' LINE 2 > TEXT 'indented' TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented ^
Der Hauptunterschied besteht darin, dass wir keine Null-Ausdrücke mehr haben: Diese werden beim Absenken in Listen von IR-Ausdrücken umgewandelt. Eigentlich ist das alles, was der Tieferlegungspass bewirkt:
TEXT 'hello' LINE 2 TEXT 'indented' > TEXT ' world' LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
Wir müssen zuerst IrExprs definieren.
Das dürfte Ihnen bekannt vorkommen:
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' > LINE 0 TEXT 'goodbye' == OUTPUT == hello __indented world ^
Alle niedrigeren Aufgaben bestehen darin, Nil()-Instanzen durch eine leere Liste ([]) und Concat(car, cdr)-Instanzen zu ersetzen, indem die Ergebnisse der Reduzierung der car- und cdr-Ausdrücke angehängt werden. Die gleiche Behandlung wird auf die Unterausdrücke in Nest und Group angewendet. Dies ist nichts weiter als eine rekursive „Verflachungs“-Operation.
TEXT 'hello' LINE 2 TEXT 'indented' TEXT ' world' LINE 0 > TEXT 'goodbye' == OUTPUT == hello __indented world goodbye ^
Testen Sie unten mit einem unserer Beispiel-Doc-Ausdrücke von oben:
AsmInst = str | int
Das ist genau das, was wir erwarten.
Jetzt zum letzten Schritt: Kompilieren. Diese Funktion wandelt IR-Ausdrücke in ASM-Anweisungen um und berücksichtigt dabei die maximale Ziellinienbreite:
def interpret(insts: list[AsmInst]) -> str: """Interpret the ASM instructions as a string.""" result = "" for inst in insts: match inst: case text if isinstance(text, str): result += inst case indent if isinstance(indent, int): result += f"\n{' ' * indent}" return result
Hier ist die grobe Idee des Algorithmus:
['onions', ['carrots', 'celery'], 'turnips']
Im Prozess geschieht die Magie:
group( '[' + nest( 4, br() + "'onions'" + ',' + br(' ') + group( '[' + nest(4, br() + "'carrots'" + ',' + br(' ') + "'celery'") + br() + ']' ) + ',' + br(' ') + "'turnips'" ) + br() + ']' )
Kurz gesagt:
Wie funktioniert fit_flat? Es geht einfach die Anweisungen in der Gruppe durch, behandelt brs als Text und stoppt, wenn entweder:
TEXT '[' LINE 4 TEXT "'onions'" TEXT ',' LINE 4 TEXT '[' TEXT '' TEXT "'carrots'" TEXT ',' TEXT ' ' TEXT "'celery'" TEXT '' TEXT ']' TEXT ',' LINE 4 TEXT "'turnips'" LINE 0 TEXT ']'
Endlich können wir die Teile zusammenfügen:
[ 'onions', ['carrots', 'celery'], 'turnips' ]
Die einzigen verbleibenden Teile der Pretty-Printer-Schnittstelle sind die Dokumentausdruckskonstruktoren:
[ 'onions', [ 'carrots', 'celery' ], 'turnips' ]
Lassen Sie uns das Beispiel aus der Einleitung ausprobieren:
['onions', ['carrots', 'celery'], 'turnips']
Die vollständige Quelle finden Sie hier.
? Im Bau ?
? Im Bau ?
Das obige ist der detaillierte Inhalt vonPretty-Printing ist Zusammenstellung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!