Heim  >  Artikel  >  Backend-Entwicklung  >  Pretty-Printing ist Zusammenstellung

Pretty-Printing ist Zusammenstellung

DDD
DDDOriginal
2024-11-01 04:21:02511Durchsuche

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.

Überblick

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:

Pretty-Printing is Compilation

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']

Drucker-Assemblersprache

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:

  1. Ausgeben einer Textzeichenfolge.
  2. Weiter zur nächsten Zeile und Einrücken um einen bestimmten Betrag.

Daher besteht ASM nur aus zwei Anweisungen:

  1. TEXT , der eine Textzeichenfolge ausgibt.
  2. LINE , wodurch der Drucker zur nächsten Zeile vorrückt und dann um Einrückungsräume einrückt.

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:

  • TEXT-Anweisungen werden durch Python-Strings dargestellt.
  • LINE-Anweisungen werden durch Ints dargestellt.

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
          ^

Die Doc-Sprache (Teaser)

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?

  • Ein Python-Str-Literal repräsentiert sich selbst.
  • br() ist ein möglicher Zeilenumbruch.
  • nest(indent, doc) erstellt einen „verschachtelten“ Unterausdruck, der optisch durch Einrückungsräume ausgeglichen wird.
  • group(doc) begrenzt einen Unterausdruck, in dem alle br()s entweder als Zeilenumbrüche behandelt werden oder nicht.
  • kombiniert Doc-Ausdrücke.
  • nil fungiert als „leerer“ Ausdruck.

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.

Eine Zwischendarstellung

Unser Schaltplan, der den Pretty-Printing-Prozess beschreibt, war also etwas zu stark vereinfacht. Hier ist das vollständige Bild:

Pretty-Printing is Compilation

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.

Der Compiler (endlich)

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:

  • Der Compiler verwaltet einige „Status“-Informationen:
    • Die aktuelle (horizontale) Linienposition.
    • Der aktuelle Einrückungsbetrag.
    • Gibt an, ob brs als Zeilenumbrüche behandelt oder „flach“ dargestellt werden sollen.
  • Wir durchlaufen die Ausdrücke, geben einige ASM-Anweisungen aus und aktualisieren die Zeilenposition entsprechend.
['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:

  • Für Textausdrücke geben wir eine TEXT-Anweisung aus und erhöhen die Position (pos) um die Länge des Textes.
  • br-Ausdrücke werden abhängig vom Wert von flat behandelt:
    • Wenn „flat“ wahr ist, behandeln Sie sie als Text.
    • Andernfalls geben Sie eine INDENT-Anweisung mit der aktuellen Einrückungsebene aus und setzen die Position auf diesen Wert zurück.
  • Für Verschachtelungsausdrücke verarbeiten wir alle Unterausdrücke, wobei die aktuelle Einrückungsebene jedoch um den Einrückungswert des Verschachtelungsausdrucks erhöht wird.
  • Abschließend prüfen wir bei Gruppenausdrücken zunächst, ob die gesamte Gruppe flach dargestellt werden kann, ohne den verbleibenden Platz zu überschreiten. Dies bestimmt den Wert von flat für alle gruppierten Unterausdrücke, der wiederum darüber entscheidet, ob brs als Zeilenumbrüche (oder als Text) gerendert werden.

Wie funktioniert fit_flat? Es geht einfach die Anweisungen in der Gruppe durch, behandelt brs als Text und stoppt, wenn entweder:

  • Uns ist der Speicherplatz ausgegangen (Breite < 0). In diesem Fall können die gruppierten Unterausdrücke nicht flach gerendert werden.
  • Wir haben alle Unterausdrücke verarbeitet. In diesem Fall kann die Gruppe flach gerendert werden.
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 ']'

Alles zusammenfügen

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.

So „programmieren“ Sie in Doc

? Im Bau ?

  • Gemeinsame Muster.
  • Wie Br, Nest und Gruppe interagieren.

Glocken und Pfeifen

? Im Bau ?

  • Hinzufügen einer Faltparametergruppe zum Erzwingen Falten.

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!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn