Um ein (verschachteltes) Objekt komfortabel untersuchen zu können, wird eine gute Darstellung desselben in Form von Text benötigt. Pretty-Printer versuchen genau das: Dinge so auf den Bildschirm zu drucken, dass die interne Struktur auf einen Blick ersichtlich ist. Wir schauen uns heute den Pretty-Printer aus Philip Wadlers Paper „A prettier printer“ (1997) an, dessen Besonderheit die Anwedung von Algebra auf Design und Implementierung ist. Daran werden wir ein weiteres Mal sehen, dass Datenmodellierung und Abstraktion der funktionalen Programmierung zu wunderbar einfachem und prägnantem Quellcode führen, der komplexe Domänen elegant beschreibt.

*Hinweis: Der komplette Code des Pretty-Printers ist auf Github zu finden. Viel Spaß beim Ausprobieren!

The Good, the Bad and the Ugly

Wir werden im Folgenden LISP-Ausdrücke betrachten. Bei ihnen ist es von besonderer Wichtigkeit, die interne Struktur ersichtlich zu machen und damit den Sinn von guten Pretty-Printern zu motivieren. Hier ein gelungenes, ein nicht ganz gelungenes und ein ganz und gar nicht gelungenes (Pretty-)Printing der Fakultätsfunktion:

(DEFUN FACTORIAL (X)
       (COND ((= X 0) 1)
             ((* (FACTORIAL (- X 1))
                 X))))

(DEFUN 
 FACTORIAL
 (X)
 (COND 
  ((= X 0) 1)
  ((* (FACTORIAL 
       (- X 1))
      X))))

(DEFUN FACTORIAL (X) (COND ((=
X 0) 1) ((* (FACTORIAL (- X 1)
) X))))

Fangen wir unten an: Es wurde kein Pretty-Printer verwendet, sondern der Code einfach auf die verfügbare Breite von 30 Zeichen gedruckt. Sehr ungeschickt ist, dass wegen fehlender Einrückung nicht ersichtlich ist, welcher Ausdruck Teil eines anderen Ausdrucks ist. Im zweiten Beispiel ist dies schon wesentlich besser, da verschachtelte Ausdrücke jeweils um ein Zeichen eingerückt sind. Zudem erzeugen die Zeilenumbrüche noch mehr Struktur. Hier ist jedoch zu bemängeln, dass zu „aggressiv“ umgebrochen wird. Das hat schlechtere Lesbarkeit und mehr Zeilen im Pretty-Print zur Folge. Das erste Beispiel lässt den Kopf und Rumpf einer Funktionsdefinition besonders gut erkennen.

Als weiteres Beispiel XML-Code in drei geprinteten Varianten (dieses Mal mit 30 Zeichen Maximalbreite):

<p
  color="red" font="Times"
  size="10"
>
  Here is some
  <em> emphasized </em> text.
  Here is a
  <a href="http://ww.eg.com" >
    link
  </a>
  elsewhere
</p>

<p
  color=
    "red" 
  font=
    "Times"
  size=
    "10"
>
Here is some
<em> 
  emphasized 
</em> 
text. Here is a
<a
  href="http://www.eg.com/"
> 
link 
</a>
elsewhere.
</p>

<p color="red" font="Times"
size="10" > Here is some <em>
emphasized </em> text. Here is
a <a href="http://www.eg.com/"
> link </a> elsewhere. </p>

Das erste Beispiel zeigt wieder den Idealfall: Der gesamte em-Ausdruck wird in eine Zeile in den Fließtext eingebettet. Es ist kein Umbruch nötig, da die Maximalbreite nicht überschritten wird. Die Attribute des p-Elements werden zunächst nebeneinander, so lang es Platz hat, und dann untereinander sortiert. Hier wird also nicht nur stur ein „Vorgehen“ befolgt („breche pro Attribut-Wert-Paar um“), sondern der vorhandene Platz sinnvoll ausgenutzt. Die beiden anderen Beispiele sind aus obigen Gründen keine gelungenen Layouts.

Obwohl die Beispiele aus zwei unterschiedlichen Domänen kommen (Programmiersprache Common Lisp und Auszeichnungssprache XML), können beide mit dem hier vorgestellten Pretty-Printer behandelt werden. Das wird dadurch ermöglicht, dass eine Metasprache für Dokumente, die schön ausgedruckt werden sollen, entwickelt wird. Dann erst wird für den Anwendungsfall bestimmt, wie man Objekte in diese Metasprache übersetzt. Die Vorgehensweise, eine beschreibende Zwischensprache zu entwerfen – eine sogenannte domänenspezifische Sprache (domain specific language im Englischen, kurz DSL), ist in der funktionalen Programmierung gang und gäbe. Im Blogpost Systematisch eingebettete DSLs entwickeln in Clojure werden DSLs ausführlich beschrieben.

Strategie des Prettier Printers

Der Pretty-Printer nimmt eine Beschreibung eines Dokuments entgegen und erzeugt daraus viele Layouts. Diese unterscheiden sich im Format bzgl. Umbrüchen und Einrückungen. Dann wählt der Pretty-Printer aus dieser Menge von Layouts das beste aus. Das beste hierbei ist das, das jede Zeile möglichst gut ausnutzt, aber dennoch die vorgegebene Breite nicht überschreitet.

In diesem Blogpost wollen wir unser Augenmerk auf die Verwendung der Pretty-Printer-DSL setzen. Wie die eigentliche DSL implementiert ist – besonders in Hinblick auf Effizienz (es könnten einige Tausend bis Millionen Layouts entstehen) – sehen wir in einem folgenden Blogpost.

Die Pretty-Printer-Dokumentensprache

Unsere Pretty-Printer-DSL besteht aus Dokumenten und Kombinatoren, das heißt Operatoren, die auf diesen Dokumenten arbeiten. Wir benutzen ab jetzt Haskell-Syntax, um die Funktionssignaturen und Typen zu beschreiben.

Sechs Operatoren für das Glück

Folgende fünf Operatoren stehen zur Verfügung, um ein Dokument in unserer Pretty-Printer-Sprache zu beschreiben:

(<>)       :: Doc -> Doc -> Doc
nil        :: Doc
text       :: String -> Doc
line       :: Doc
nest       :: Int -> Doc -> Doc

Dabei fügt <> zwei Dokumente aneinander (man sagt auch es „konkateniert“ sie). Das Resultat ist wieder ein Dokument. nil ist das sogenannte leere Dokument. Beliebige Dokumente bleiben bei Konkatenation mit nil unverändert (etwas mathematischer ausgedrückt bildet also die Menge aller Dokumente zusammen mit der assoziativen Verknüpfung <> und dem neutralen Element nil ein Monoid). text wandelt eine Zeichenkette in ein Dokument um, line beschreibt einen Zeilenumbruch und nest rückt ein Dokument ein.

Hinzu kommt ein sechster Operator, layout, der ein als String gerendertes Dokument zurückgibt:

layout     :: Doc -> String

An den fünf Operatoren und layout ist die in der funktionalen Programmierung häufig vorkommende Trennung von Beschreibung und Ausführung wunderbar ersichtlich.

Das folgende, mit den obigen Operatoren beschriebene Dokument

doc =
  text "<p>" <> 
  nest 2 (
    line <> text "<a>" <> text "This is a link" <> text "</a>" <>
    line <> text "Some text"
  ) <>
  line <> text "</p>"

wird mit Hilfe von layout so ausgedruckt:

<p>
  <a>This is a link</a>
  Some text
</p>

Erst die Masse, dann die Klasse

Oben war die Rede von mehreren Layouts, aus denen das beste ausgewählt wird. Hier ist bisher nichts davon zu sehen. Die Sprache benötigt dafür noch eine Erweiterung des Begriffs „Dokument“ und eine weitere Operation: Wenn ab jetzt „Dokument“ gesagt wird, meint dies eine Sammlung mehrerer möglicher Layouts (desselben Dokuments). Diese müssen jedoch von derselben Struktur sein und dürfen sich nur in Zeilenumbrüchen und Einrückungen unterscheiden. Doch wie kommen wir überhaupt zu verschiedenen Variationen eines Layouts? group ist hier der Schlüssel:

group :: Doc -> Doc

group nimmt ein Dokument (Sammlung mehrerer Layouts) und fügt dieser Sammlung ein neues Layout hinzu, in welchem jeder Zeilenumbruch durch ein Leerzeichen ersetzt wird. Wenn wir also group auf unser oben definiertes Dokument doc anwenden, käme das folgende Layout hinzu (nach Anwendung von layout):

<p> <a>This is a link</a> Some text </p>

Unser Pretty-Printer stellt zusätzlich die Funktion pretty bereit:

pretty :: Int -> Doc -> Doc

Diese wählt aus der übergebenen Sammlung von Layouts das beste für die ebenfalls übergebene Maximalbreite aus. Hier zur Verdeutlichung obiges Dokument im Zusammenspiel mit pretty und verschiedenen Maximalbreiten:

twoLayouts = group doc

layout $ pretty 30 twoLayouts

-- --> 
-- <p>
--   <a>This is a link</a>
--   Some text
-- </p>

layout $ pretty 60 twoLayouts

-- -->
-- <p> <a>This is a link</a> Some text </p>

Das einzeilige Layout ist für obigen Aufruf zu lang. Im unteren Aufruf unterschreiten beide Layouts die geforderte Maximalbreite. Somit wird hier das einzeilige gewählt, da dieses die (erste) Zeile größtmöglich ausnutzt.

XML aufgehübscht!

Zum Schluss des Blogposts wollen wir uns noch anschauen, wie solch eine Übersetzung von Domänencode, hier im speziellen XML-Code, in die Pretty-Printer-DSL aussehen könnte. Wenn wir tatsächlichen XML-Code benutzen wollten, müssten wir diesen zunächst parsen, um ihn in einer für uns handhabbaren Form zu haben. Wir nehmen der Einfachheit halber an, dies sei bereits geschehen und die Repräsentation von XML-Code sähe wie folgt aus:

data XML = Element String [Attribute] [XML]
         | Txt String

data Attribute = Attribute String String

Ein XML-Element ist ein gemischter Datentyp. Entweder es ist ein tatsächliches Element, das aus einer Zeichenkette (das ist der Name des Elements), einer Liste an Attributen und einer Liste an weiteren XML-Elementen besteht. Oder es ist nur eine einfache Zeichenkette. Ein Attribut besteht aus zwei Zeichenketten: dem Namen des Attributs und dessen Wert. Die Repräsentation von <p color="red"> Hallo </p> ist demnach:

Element "p" [Attribute "color" "\"red\""] [Txt "Hallo"]

Fangen wir mit den Attributen an. Ein Attributpaar soll immer in eine Zeile gedruckt werden:

attToDOC :: Attribute -> DOC
attToDOC (Attribute key value) = text key <> text "=\"" <> text value <> text "\""

Ein Tag wollen wir so drucken lassen, dass es nur dann auf eine Zeile gedruckt wird, falls alle Attribute auf diese passen. Ansonsten soll nach dem Tag-Namen umgebrochen und um zwei Zeichen eingerückt werden. Zudem soll dann auch nach jedem Attributpaar umgebrochen werden:

tagToDOC :: String -> [Attribute] -> DOC
tagToDOC name [] = text "<" <> text name <> text ">"
tagToDOC name attributes = group
                           (text "<" <> text name <>
                            (nest 2 (line <> concatDOCs (map attToDOC attributes))) <>
                            line <> text ">")

group um den gesamten Ausdruck macht die Wahl zwischen „alles auf eine Zeile“ vs „Umbrechen nach jedem Attributpaar“ möglich, da group, wie oben beschrieben, den Layouts ein weiteres hinzufügt, bei welchem alle Zeichenumbrüche entfernt sind. concatDOCs nimmt eine Liste von Dokumenten und fügt diese mit einem Zeilenumbruch dazwischen zu einem Dokument zusammen:

concatDOCs :: [DOC] -> DOC
concatDOCs []      = nil
concatDOCs [x]     = x
concatDOCs (x:xs)  = x </> concatDOCs xs

</> ist eine Hilfsfunktion, die zwei Dokumente mit einem line-Dokument dazwischen konkateniert. Jetzt nehmen wir obige Funktionen zusammen, um ein ganzes Element auszudrucken:

xmlToDOC :: XML -> DOC
xmlToDOC (Txt string)                    = text string
xmlToDOC (Element name attributes xmls)  = group
                                           (tagToDOC name attributes </>
                                            nest 2 (group line <>
                                                    concatDOCs (map xmlToDOC xmls)) </>
                                            text "</" <> text name <> text ">")

Im einfachen Textfall erzeugen wir nur ein TEXT-Dokument. Wenn wir ein Element vorliegen haben, lassen wir das Tag drucken, dann ggf. umbrechen und um zwei Zeichen einrücken, um dann die Kinder-Elemente entsprechend rekursiv drucken zu lassen.

Das Dokument

xml   = Element "p" [
          Attribute "color" "red",
          Attribute "font" "Times",
          Attribute "size" "10"
        ] [
          Txt "Here is some",
          Element "em" [] [Txt "emphasized"],
          Txt "Text. Here is a",
          Element "a" [Attribute "href" "http://example.org"]
            [Txt "link"],
          Txt "elsewhere."
          ]

wird mit layout $ pretty 30 xml bzw. layout $ pretty 60 xml so ausgedruckt:

<p
  color="red"
  font="Times"
  size="10"
>
 Here is some
  <em>  emphasized </em>
  text. Here is a
  <a
    href="http://example.org"
  >
   link
  </a>
  elsewhere.
</p>


<p color="red" font="Times" size="10" >
 Here is some
  <em>  emphasized </em>
  text. Here is a
  <a href="http://example.org" >  link </a>
  elsewhere.
</p>

Wie wir sehen, kommt diese Implementierung noch nicht ganz an die Beispiele von oben heran: Dort wurden mehrere Attributpaare (wenn möglich) sogar auf eine Zeile gedruckt. Zudem gibt es unschöne Leerzeichen an gewissen Stellen. Wie man das elegant verbessern kann, werden wir im nächsten Blogpost sehen, wenn wir uns dort Hilfs- und Komfortfunktionen für unseren Pretty-Printer schreiben.

Fazit

Der heutige Blogpost zeigt, dass es nur wenige Operatoren braucht, um eine mächtige Sprache zu entwickeln. Das besonders Schöne an der Sprache ist, dass, wie so oft in der funktionalen Programmierung, kleinere Dinge zusammengefügt werden zu größeren. In einem nächsten Blogpost schauen wir uns an, wie der Pretty-Printer genau implementiert ist und optimieren unser XML-Beispiel.