Fachbereich 12 Mathematik und Informatik Philipps-Universität Marburg Autor: Johannes Juch Matr.-Nr.: 1307290 Rosenstraße 7 35037 Marburg E-Mail: [email protected] Tel.: 06421/23538 Generische funktionale Programmierung 2 Template Haskell Tim Sheard, Simon Peyton Jones: "Template Meta-programming for Haskell" Ian Lynagh: "Template Haskell: A Report From The Field" Seminararbeit von Johannes Juch Im Rahmen des Seminars „Konzepte für Programmiersprachen“ unter der Leitung von Prof. Dr. Rita Loogen Kurzzusammenfassung Mit Template Haskell wurde die rein funktionale Sprache Haskell erweitert, um MetaProgrammierung zur Compile-Zeit zu ermöglichen. In dieser Arbeit werden die Mittel und Werkzeuge zur algorithmischen Konstruktion von Programmen mit Template Haskell vorgestellt. Es wird gezeigt, wie man mit Template Haskell Code zur Compile-Zeit generiert und sich ein erzeugtes Programm bestimmten Anforderungen anpasst, z.B. der Größe der zu bearbeitenden Datenstruktur. Danach werden vermeintliche Probleme ,Vor- und Nachteile diskutiert, sowie Ansätze zur deren Lösung oder Umgehung. KAPITEL 1 EINLEITUNG..................................................................................... 2 1.1 Motivation ........................................................................................................................... 2 1.2 Grundlagen und Aufbau der Arbeit................................................................................. 2 KAPITEL 2 TEMPLATE HASKELL: DIE NEUEN FÄHIGKEITEN....................... 4 2.1 Quasi Quotation und der Splice-Operator '$' ................................................................. 4 2.2 Die Syntax-Generator Funktionen und die Quotation Monade .................................... 6 2.3 Deklarationen und Vergegenständlichung (Reification) ................................................ 6 KAPITEL 3 TYPISIERUNG IN TEMPLATE HASKELL........................................ 7 3.1 Ausdrücke ........................................................................................................................... 8 3.2 Deklarationen ................................................................................................................... 10 4 BEISPIELE UND ERLÄUTERUNGEN.................................................................. 11 4.1 Quasi Quotation vs Syntax-Generator Funktionen ...................................................... 11 4.2 Scoping .............................................................................................................................. 12 4.3 Eigenschaften der Quotation Monade............................................................................ 14 5 TEMPLATE HASKELL IN DER ANWENDUNG ................................................... 16 5.1 Expandieren von Deklarationen des FFI ....................................................................... 16 5.2 Optimierung durch "Unrolling"..................................................................................... 17 KAPITEL 6 FAZIT .............................................................................................. 19 ANHANG A: ALGEBRAISCHE DATENTYPREPRÄSENTATION VON HASKELL 21 ANHANG B: MONADISCHE SYNTAX-OPERATOREN.......................................... 22 1 Kapitel 1 Einleitung 1.1 Motivation Meta-Programmierung zur Compile-Zeit und die dadurch mögliche algorithmische Konstruktion von Programmen zur Compile-Zeit eröffnen dem Programmierer die Möglichkeit zur Entwicklung von polymorphen Programmen, da der Typ eines durch MetaProgramm erzeugtes Programms erst zur Compile-Zeit des Meta-Programms fest steht. Des Weiteren können Benutzer gerichtete Optimierungen in der Compilierung durchgeführt werden. In der Vergangenheit wurde viel mit Laufzeit Meta-Programmierung experimentiert, während die Compile-Zeit Meta-Programmierung weniger Interesse auf sich zog (Ausnahme: Scheme community). Innerhalb kurzer Zeit haben sich die Art und Weisen, wie "Templates" in der Programmierung verwendet werden, beeindruckend ausgeweitet. Viele interessante Optimierungen sprechen leider eine zu kleine Zielgruppe an, um die Kosten ihrer Entwicklung zu rechtfertigen. Die Alternative ist es, dem Programmierer die Möglichkeit zu geben seine Programme gezielt zu manipulieren und so eigene Optimierungen mit denen des Compilers zu verknüpfen. Der Programmierer erhält die Möglichkeit, dem Compiler verschiedene Tricks durch seine Programme beizubringen, was eine individuelle und flexiblere Optimierung bedeutet. Oft ist es auch einfacher ein Programm zu schreiben, das ein Programm schreibt, als das Programm direkt zu schreiben. Mit Template Haskell erhält der Programmierer die Möglichkeit seine Programme, oder Teile davon, berechnen zu lassen, als sie selbst zu schreiben. In der Vorstellung der Entwickler könnte Template Haskell für viele Sachen und Probleme verwendet werden, u.a. um • algorithmische Konstruktion von Programmen • Abstraktionen, die über den Abstraktionsgrad der Sprache hinaus gehen • bedingte, programmgesteuerte Compilierung zu ermöglichen. 1.2 Grundlagen und Aufbau der Arbeit Die Grundlagen für den Entwurf von Template Haskell sind zum ersten die rein funktionale und streng typisierte Sprache Haskell, und eine Erweiterung in Form eines SchablonenSystems. Die Erweiterung kann sowohl als ein Schablonen-System, als auch als typsicheres Makrosystem, für die Basissprache Haskell gesehen werden. Da Haskell eine streng typisierte 2 Sprache ist, benötigt man eine Typüberprüfung auf verschiedenen Ebenen, um die generelle Typüberprüfung der Module mit denen der Compile-Zeit-Berechnungen zu synchronisieren (Kapitel 4). Diese Ebenenüberprüfung sorgt dafür, dass alle Laufzeitberechnungen auf ihre Typenkonformität überprüft wurden und somit sicher sind. Zusätzlich werden von der Ebenenüberprüfung Codegeneratoren bereitgestellt, die, als gewöhnliche Programme geschrieben, die Einführung von abhängigen Typen erfordern würden. Durch diese Codegeneratoren in der Typüberprüfung bleibt die Sprache ausdrucksstark und einfach. Es werden neue syntaktische Elemente und Annotationen eingeführt, die notwendig sind, um festlegen zu können, welche Programmteile (Ausdrücke, Deklarationen,...) wann ausgeführt werden sollen. Zu diesen gehören die sogenannten "Quasi-Quotations" und die "Splices", die mit dem Symbol $ eingeleitet werden (Kapitel 2). Die Repräsentation von gewöhnlichem Haskell Quellcode geschieht über neu eingeführte algebraische Datenstrukturen. Dies und die Tatsache, dass in diesem Ansatz die Sprache der Erweiterung mit der Basissprache übereinstimmt, eben Haskell, ermöglicht es, die bereits existierenden Mechanismen (Fallunterscheidung, Pattern Matching) zu nutzen, um Code zu generieren, kontrollieren und zu manipulieren. Weitere Meta-Programm-Fähigkeiten werden durch die sogenannte Quotation Monade bereitgestellt. Eine monadische Bibliothek, die auf algebraischen Datentypen aufsetzt, stellt monadische Syntaxoperatoren bereit. Diese Bibliothek kapselt Meta-Programm-Eigenschaften, wie Fehlerausgabe, Namengenerierung und Programmverdinglichung (in dieser Arbeit weiterhin Reification genannt), und stellt eine einfache Schnittstelle zu deren Nutzung bereit. Die oben bereits erwähnten "Quasi Quotations" setzen wiederum auf der monadischen Bibliothek auf. Sie übersetzen Code in eine monadische Berechnung, was die Erhaltung statischer Gültigkeiten einschließt. Programm-Reification kann dazu eingesetzt werden, die vom Programmierer geschriebenen Komponenten, auf ihre syntaktische Struktur zu analysieren und zu manipulieren. Dies ist vor allem dann hilfreich, wenn man sogenannten "boilerplate"1 Code direkt von der Datentypdeklaration ableiten möchte. „Boilerplate“ steht für Code der zur Traversierung von Datenstrukturen benötigt wird. Dieser kann gerade bei wechselseitig rekursiven Datentypen sehr umfangreich und bei Änderungen der Strukturen sehr fehleranfällig sein. Deshalb ist es sinnvoll diesen Code direkt und automatisch von der Datenstruktur abzuleiten, so dass sich der Programmierer auf die wesentlichen Details seines Programms konzentrieren kann. 1 Mehr zu boilerplate Code in: "Scrap your boilerplate: a practical design pattern for generic programming” by Ralf Lämmel and Simon Peyton-Jones, erschienen in Sitzungsbericht der TLDI 2003, ACM Press. 3 Kapitel 2 Template Haskell: Die neuen Fähigkeiten In diesem Kapitel werden die grundlegenden Mittel zur Template Meta-Programmierung mit Template Haskell vorgestellt und mit Beispielen deren Nutzen aufgezeigt. 2.1 Quasi Quotation und der Splice-Operator '$' Die „Quasi Quotes“ beinhalten gewöhnliche Haskell Syntax Fragmente, sie sind die eigentlichen "Schablonen" in Template Haskell. Der Quasi-Quote Mechanismus setzt auf der monadischen Bibliothek, die Template Haskell mitbringt, auf. Die Quasi Quotations haben immer einen der Typen ExpQ, DecQ oder Type d.h. sie "heben" den Haskellcode in ihrem Inneren auf diesen monadischen abstrakten Datentyp. Es sei hier schon erwähnt, dass der Typ ExpQ ein Synonym für Q Exp ist. Quasi Quotations dürfen folgenden Inhalt haben: • [| ... |], wobei "..." ein Ausdruck ist; die Quotation hat dann den Typ ExpQ. • [d| ... |], wobei "..." eine Liste von Deklarationen höchster Ebene ist; die Quotation hat dann Typ Q [Dec]. • [t| ... |], wobei "..." ein Typ ist; die Quotation hat dann Typ Type. Die "Quasi Quotes" übersetzen die Ausdrücke in Code auf Objektebene, der bei letztendlicher Ausführung des Programms zu einer Repräsentation der Ausdrücke in Form abstrakter Syntaxbäume führt, die im Modul Language.Haskell.TH.THSyntax definiert sind. Ein „Splice“ kann an verschiedenen Stellen gesetzt werden und besagt lediglich, dass der einbezogene Code zur Compile-Zeit ausgeführt werden soll. Dabei kann der Code entweder • ein Ausdruck, der den Typ Q Exp hat, • eine Liste von Deklaration auf höchster Ebene, die den Typ Q [Dec] hat, • oder ein Typ, der den Typ Type hat sein. Es ist auf die syntaktische Besonderheit zu achten, dass kein Leerzeichen zwischen dem $ und dem zu „splicenden“ Code sein darf, sonst wird das „Splice-Symbol“ vom Compiler nicht als solches erkannt. In meinem ersten Beispiel für die Verwendung von [| ... |] und '$' geht es um die Ausgabe des aktuellen Datums, in Form einer printf Funktion, wie es sie in C gibt. printf "Heute ist der %d. %s %d" day month year Die Funktion printf bekommt einen String und zwei weitere Parameter übergeben, die den String vervollständigen. In Haskell können wir diese Funktion nicht implementieren, in Template Haskell verwenden wir den $-Operator folgendermaßen: $(printf "Heute ist der %d. %s %d") day month year 4 Nach Typüberprüfung und Compilierung erhalten wir den Ausdruck: ( \d0 -> \s1 -> \d2 -> "Heute ist der " ++ d0 ++ ". " ++ s1 ++ d2) Die Lambda Abstraktion wird nun erneut einer Typüberprüfung unterzogen und auf day, month und year angewandt. Natürlich fehlt uns jetzt noch die Deklaration der Meta-Funktion printf: printf :: String -> ExpQ printf s = gen (parse s) Die Hilfsfunktion parse zerlegt den formatierten String in eine Liste des Datentyps Format. parse ist eine gewöhnliche Haskell Funktion, die Funktion gen dagegen verwendet Quasi Quotation: data Format = D | S | L String parse :: String -> [Format] parse s = ... gen :: [Format] -> ExpQ -> ExpQ gen [] ex = ex gen (D :xs) ex = [| d -> $(gen xs [| $ex++show d |]) |] gen (S :xs) ex = [| s -> $(gen xs [| $ex++s |]) |] gen (L s :xs) ex = gen xs [| $ex ++ $(lift s) |] Durch die $-Operatoren werden die Aufrufe von gen zur Compile-Zeit ausgeführt und in die Quasi Quotations eingefügt. Das zweite Argument (ex) benötigen wir als Akkumulator für die rekursiven Aufrufe von gen. Im obigen Beispiel zum Aufruf von printf sieht die Auswertung folgendermaßen aus. Nachdem die Hilfsfunktion parse das erste Listenelement vom Typ Format erzeugt hat, wird die Funktion gen mit diesem Aufgerufen. Die Funktion gen erzeugt Code der durch die "Splices" innerhalb der Quasi Quotations dafür sorgt, dass die Auswertung des erzeugten Codes ebenfalls zur Compile-Zeit stattfindet. Die Variablen innerhalb der zweiten Argumente der rekursiven Aufrufe von gen sind eindeutig über die Lambda Abstraktionen gebunden (vgl. S.13). Eine der globalen Prinzipien von Template Haskell ist, dass die Quatation – Splice Aufhebung immer gilt • bei Ausdrücken: $[| Exp |] = Exp • bei Deklarationen: $[| Dec |] = Dec • und bei Typen: $[| Typ |] = Typ. 5 2.2 Die Syntax-Generator Funktionen und die Quotation Monade In Kapitel 4.1 werde ich zeigen, dass wir mit den Quasi Quotations und dem Splice-Operator nicht den vollen Umfang der Möglichkeiten der Meta-Programmierung erreichen. Wir benötigen eine Methode, mit der wir Haskell Syntaxbäume direkt „per Hand“ konstruieren können, um flexibel Code zu generieren. Diese Methode stellt Template Haskell in Form einer ganzen Familie von Syntax-Generator Funktionen bereitstellt. Auszüge aus dem Modul Language.Haskell.TH.THSyntax kann im Anhängen A und B eingesehen werden. Wie wir noch sehen werden, bezahlen wir den Gewinn an Flexibilität mit Übersichtlichkeit des Codes, da die Syntax-Generator Funktionen eine Abstraktion von Haskellcode darstellen. Die Quotation Monade Q kapselt verschiedene Fähigkeiten, wie die Erzeugung neuer Namen (vgl. Kapitel 4.3), Fehlerbehandlung und Input/Output-Methoden. Eine Bibliothek stellt eine einfache Schnittstelle für Zugriffe auf die untere Ebene der algebraischen Datentypen bereit. 2.3 Deklarationen und Vergegenständlichung (Reification) Die Vergegenständlichung von Datenstrukturen, die in dieser Arbeit weiterhin Reification genannt wird, ermöglicht es Programmen, deren eigene Strukturen zur Compile-Zeit zu untersuchen. Das heißt, dass dem Programmierer erlaubt wird, auf die compilerinternen Symboltabellen zuzugreifen. Reification ist ein nützliches Instrument, z.B. um so genannten „boilerplate“ Code zu erzeugen, oder nützliche Fehlermeldungen zu erzeugen. "Boilerplate" Code wird z.B. mit der "deriving" Anweisung selbstständig vom Compiler erzeugt, was allerdings nur für einige Standardklassen (Eq, Ord,...) unterstützt wird. Durch Template Haskells Fähigkeit zur Reification kann man algorithmisch Code erzeugen, der von der Struktur einer Datenstruktur abhängt. Es werden vier Operatoren zur Reification vorgestellt: reifyType, reifyDecl, reifyFixity, und reifyLocn, die jeweils ein Ergebnis in der Quotation Monade liefern. • reifyType bekommt den Bezeichner einer Funktion oder eines Datenkonstruktors und gibt den Typ der Dateneinheit in Form eines Wertes vom Typ TH.Syntax.Type zurück. • Analog bekommt reifyDecl den Bezeichner eines Typs und gibt eine Repräsentation (eine Datenstruktur) der Deklaration des Typs in Form eines Wertes vom Typ TH.THSyntax.Decl zurück. • reifyLocn gibt die Position im Quelltext, in der es sich befindet, in Form eines Wertes vom Typ Q String zurück. • reifyFixity gibt die Präzedenz seines Argumentes zurück. 6 In der zweiten Version von Template Haskell, die im November 2003 erschien, hat sich Reification stark verändert. In der ersten Version war Reification ein Sprachkonstrukt und keine Funktion. Jetzt gibt es noch eine einfache Funktion reify, die den Namen übergeben bekommt und Informationen über das Element aus der Symboltabelle und Typinformationen des Compilers zurückgibt. reify :: Name -> Q Info Name ist ein neuer abstrakter Datentyp, und Info ist ebenfalls ein neu eingeführter Datentyp über dessen Konstruktoren die jeweiligen Informationen zurückgegeben werden. Zur Reification sind noch einige Fragen offen, auf die im Kapitel 4 noch eingegangen wird. Kapitel 3 Typisierung in Template Haskell Template Haskell ist streng typisiert. Gewöhnlich wird bei Programmen streng typisierter Sprachen erst die Typenüberprüfung durchgeführt und dann folgt die Compilierung. Die Typisierung eines Programms in Template Haskell stellt ein Problem dar, weil der Typ eines "Splice"-Ausdrucks zur Compile-Zeit noch von seinen Argumenten abhängen kann. Hierzu betrachten wir noch einmal das Beispiel aus Kapitel 2.1: $(printf "Heute ist der %d. %s %d") day month year In dieser Form kann der "Splice" nicht auf seinen Typ überprüft werden, da sein Typ von dem Wert seines Stringargumentes abhängt. Offensichtlich ist die Typüberprüfung mit der (compile-zeit) Ausführung eng verwoben und die Notwendigkeit von bestimmten Typisierungsregeln ergibt sich. Die Situation in Template Haskell erfordert eine Typüberprüfung auf verschiedenen Ebenen. Die Vorgehensweise in unserem Beispiel ist nun folgendermaßen: • erstens, überprüfe den Rumpf des "Splice"-Ausdrucks; hier: printf "Heute ist der %d. %s %d" • zweitens, compiliere den Rumpf, führe ihn aus und füge das Ergebnis ein; wir erhalten dann:(\d0 -> \s1 -> \d2 ->"Heute ist der " ++ d0 ++ ". " ++ s1 ++ d2) day year month • drittens, überprüfe das resultierende gesamte Programm wie gewöhnlich. Typisierungsregeln legen fest, wie, und vor allem wann, die Ausdrücke, Variablen u.a. unter Berücksichtigung ihrer Position zu typisieren sind. Dazu verwendet der Compiler Ebenen und Zustände. Während der Typüberprüfung befindet sich der Compiler immer in einem der drei Zustände C, B, oder S. 7 Abbildung 1: Typisierungszustände in Template Haskell • C (Compilieren) steht für den normalen Compilierungszustand, den der Compiler ohne Meta-Operatoren nicht verlässt. • In den Zustand B (Bracket) gerät der Compiler, wenn er in eine Quasi Quotation eintritt. • Den Zustand S (Splice) nimmt der Compiler ein, wenn er auf einen "Splice"-Ausdruck trifft. Die Typisierungszustände für Template Haskell werden in Abbildung 1 in einem Diagramm dargestellt. Beginnend Ebene 0, zählt der Compiler mit, in welcher Ebene er sich befindet. Die Ebene wird inkrementiert, falls er innerhalb einer Quasi Quotation arbeitet, und dekrementiert, falls er einen "Splice"-Ausdruck bearbeitet. Die Ebenen sind nötig, um zwischen top-level „Splices“ und „Splices“ innerhalb von Quasi Quotations unterscheiden zu können. Dies kann gut an Hand Abbildung 1 nachvollzogen werden. 3.1 Ausdrücke Abbildung 2 zeigt die Typisierungsregeln für Ausdrücke. Typisierungsregeln haben die generelle konventionelle Form: Γ ist eine Umgebung, die Variablen auf ihre Typen abbildet, e ein Ausdruck und τ ein Typ. Das s beschreibt den Zustand und n die Ebene, auf der sich der Compiler während Typüberprüfung befindet. 8 Die Regel BRACKET drückt aus, dass ein Ausdruck in Quasi-Quotes immer den Typ ExpQ hat (vgl. Kapitel 2.1) und dass der Ausdruck innerhalb der Quasi-Quotes auch geprüft wird, jedoch auf einer Ebene höher (n+1) und im Zustand B. Die Regeln ESCB und ESCS behandeln Ausdrücke, auf die ein "Splice"-Operator angewendet wird. ESCB erklärt, wie ein "Splice"-Ausdruck überprüft wird. Innerhalb einer Quasi Quotation muss lediglich geprüft werden, ob der Ausdruck, auf den der "Splice"-Operator angewendet wird, vom Typ ExpQ ist. Das sagt noch nichts aus über den Typ des Ausdrucks, zu dem der "Splice" ausgewertet wird. Der ausgewertete Ausdruck wird später mit dem gesamten Ausdruck innerhalb der Quasi Quotation überprüft. Genau dies behandelt die Regel ESCS, und zwar einen "Splice" im Zustand C und Ebene 0 zu überprüfen. Die Umgebung Γ sammelt zusätzliche Informationen über Variablen und Funktionen (Lambda-Abstraktionen) in der Form (x : (σ,m)), wobei x der Bezeichner der Variablen oder Funktion ist , σ deren Typ und m die Ebene, in der sie gebunden ist. Die Regeln LAM und VAR benutzen diese Informationen. In der Regel Var wird z.B. getestet, ob die Ebene, in der eine Variable verwendet wird, höher ist, als die Ebene in der sie angelegt wird. Abbildung 2: Typisierungsregeln für Ausdrücke in Template Haskell 9 3.2 Deklarationen Die Typisierungsregeln für Deklarationen sind in Abbildung 3 zu sehen. Sie haben die konventionelle Form: Abbildung 3: Typisierungsregeln für Deklarationen in Template Haskell Wiederum ist hier Γ die Umgebung, in der die Deklaration geprüft wird. Γ' ist die kleinste Umgebung, die die Typen der Variablen beinhaltet, die von der Deklaration gebunden werden. Uns interessiert vor allem die Regel SPLICE, die, ähnlich wie die Regel ESCS, erst den Ausdruck innerhalb des "Splice" prüft, ihn dann ausführt und dann die Deklarationen, die er erzeugt, prüft. Es ist darauf zu achten, dass der "Splice"-Operator für Deklarationen nur im Zustand C und Ebene 0 erlaubt ist. An den Typisierungsregeln und dem Diagramm kann man einige Einschränkungen erkennen, denen Template Haskell unterliegt: • Quasi Quotations dürfen nicht geschachtelt werden. • Der "Splice"-Operator darf auf Deklarationen nur auf höchster Ebene angewendet werden (top-level declarations). Das Generieren von mehreren Deklarationen in einer Gruppe wirft ein weiteres Problem auf. Falls in einem Modul mehrere "Splices" vorkommen, so ist nicht klar, in welcher Reihenfolge sie eingefügt werden. Die Typüberprüfung wird bei einer Gruppe von Deklarationen [d1, ... , dn] wie folgt durchgeführt: [d1, ... , da] Splice ea [da+1, ... , db] Splice eb ... Splice ez [dz+1, ... , dn] 10 Es wird eine gewöhnliche Abhängigkeitsanalyse durchgeführt, gefolgt von einer Typüberprüfung in der ersten Gruppe. In der resultierenden neuen Umgebung wird nun der erste "Splice" auf seine Typen überprüft und eingefügt. Das Gesamtergebnis wird noch einer Typüberprüfung unterzogen und es wird mit der nächsten Deklarationsgruppe fortgefahren. Diese Vorgehensweise implementiert die Regel SPLICE (vgl. Abbildung 3). 4 Beispiele und Erläuterungen 4.1 Quasi Quotation vs Syntax-Generator Funktionen Template Haskell Programme, die innerhalb von Quasi Quotations stehen, repräsentieren Berechnungen von gewöhnlichen algebraischen Datenstrukturen, die Haskell Programme erzeugen. Die Quasi Quotation Notation (siehe Kapitel 2.1) ist komfortabel und übersichtlich für den Programmierer, jedoch stößt dieser Mechanismus schnell an seine Grenzen. Es gibt in Haskell vordefinierte Funktionen fst :: (a, b) -> a und snd :: (a, b) -> b die für 2-Tupel die erste bzw. zweite Komponente ausgeben. Es wäre sehr mühsam für alle weiteren n-Tupel (Tripel, Quadrupel,...) diese Funktionen per Hand zu schreiben. Wir wollen Funktion, die uns diese, in Abhängigkeit ihrer Parameter zur Compile-Zeit erzeugt. sel :: Int -> Int -> ExpQ sel i n = [| \x -> case x of ...|] Wie können wir die Patternabfrage innerhalb der Quasi Quotation schreiben? Das Konstrukt ist zu unflexibel, da die Struktur des Patterns von einem Parameter abhängt, muss der Code algorithmisch während der Compilierung erzeugt werden. Wir müssen die benötigte Struktur direkt mit den expliziten Konstruktoren für Patterns und Ausdrücke erzeugen: sel :: Int -> Int -> Exp sel i n = LamE [VarP "x"] (CaseE (VarE "x") [alt]) where alt :: Match alt = simpleMatch pat rhs pat :: Pat pat = TupP (map VarP as) rhs :: Exp rhs = VarE (as !! (i-1)) as :: [String] as = ["a"++show i | i <- [1..n]] Die Methoden können frei miteinander kombiniert werden. Ein wenig intuitiver ist die folgende Version: 11 sel i n = [| \x -> $( return (CaseE (VarE "x") [alt])) |] where ... Hier kann man gut erkennen warum die Quasi Quotations für den Programmierer bequem sind: Man sieht, was man bekommt. Sie bieten allerdings noch einen weiteren Vorteil, der im folgenden Abschnitt besprochen wird. 4.2 Scoping Eines der Kernprobleme von Template Haskell ist das versehentlich falsche Binden von Variablen. Der Konstruktor VarE bekommt einen String übergeben und wandelt ihn in eine Variable vom Typ Exp auf Objektebene um. Wenn sich nun der Geltungsbereich dieser Variable mit Parametern gleichen Namens überschneidet, kann es zu Fehlern kommen, die für den Programmierer schwer vorhersehbar sind. Betrachten wir die Funktion cross2a: cross2a :: ExpQ -> ExpQ -> ExpQ cross2a f g = [| \(x,y) -> ($f x, $g y) |] Beim dem Aufruf von cross2a (VarE "x") (VarE "y") könnten die Ausdrücke (VarE x) und (VarE y) versehentlich von den lokalen Objektvariablen in der Quasi Quotation gebunden werden. Dies ist aber nicht der Fall, weil die Quasi Quotation die Objektvariablen selbstständig umbenennt. Wir erhalten bei dem obigen Aufruf folgenden Ausdruck: \(x0, y1) -> (x x0, y y1) Leider unterstützen die Syntax-Generator Funktionen die automatische Generierung neuer Namen nicht, d.h. der Programmierer muss dafür sorgen, dass Variablen eventuell umbenannt werden. Hierfür steht die Funktion gensym :: String -> Q String zur Verfügung, die neue Namen für Variablen liefert: cross2b :: Exp -> Exp -> ExpQ cross2b f g = do { x <- gensym "x" ; y <- gensym "y" ; ft <- f ; gt <- g ;return LamE [TupP [VarP "x", VarP "y"]] (TupE [AppE ft (VarE "x"), AppE gt (VarE "y")])) Wir können dieses Problem auch umgehen, indem wir die monadischen Datentypkonstruktoren der Syntax-Generator Funktionen verwenden. Sie sind der monadische Gegenpart der algebraischen Datentypkonstruktoren. Die Quasi Quotations sind sehr bequem für den Programmierer und sie verstehen sowohl die Typisierung als auch die Scoping Eigenschaften von Haskell. Genauer gesagt: Der lexikalische Gültigkeitsbereich (lexical scope) jeder Variable bezüglich ihres Auftretens im 12 Quellprogramm vor jeder Expansion ist auch nach allen Expansionen unverändert. Damit es nicht zu unbeabsichtigten Bindungen kommt, müssen die Quasi Quotations Variablen umbenennen (vgl. Bsp. cross2a). Es besteht zusätzlich die Möglichkeit, dass Konflikte mit Variablen auftreten, die außerhalb von Quasi Quotes gebunden sind. module GS ( genSwap ) where module SU where swap (a,b) = (b,a) import GS ( genSwap ) genSwap x = [| swap x |] swap = True swapUse = $(genSwap (4,5)) $(genSwap (4,5)) kann in diesem Beispiel nicht zu swap (4,5) expandiert werden, da das swap des Splices von der booleschen Konstante des Moduls SU gebunden wird. Wir können hier auf zwei verschiedene Arten vorgehen. Entweder wir deklarieren die Funktion swap in genSwap lokal in einer where-Klausel, oder wir verwenden den Originalnamen (GS:swap (4,5)). Da swap nicht importiert wird, kann man hier keinen qualifizierten Namen (GS.swap) verwenden, was ebenfalls einen Konflikt im Namensraum des Moduls GS hervorrufen würde. Originalnamen (GS:swap) dagegen sind eine Erweiterung zu Haskell, die Template Haskell verwendet, um static scoping auch für die Meta- programmierungserweiterungen implementieren zu können. Originalnamen sind Teil der Coderepräsentation, die sich eindeutig auf eine top-level Variable bezieht, die in Gültigkeitsbereichen verdeckt sind und verwendet werden sollen. Die Fähigkeit den Wert einer Variablen zur Compile-Zeit in generierten Code einzufügen, nennt man cross-stage persistence. Probleme tauchen dabei ebenfalls für verschachtelte Variablen auf, was eine andere Behandlung erfordert als bei top-level Variablen. Die Variable x ist frei in [| swap x |] und hat keine top-level Bindung im Modul GS. Während der Compilierung ist lediglich der Typ von x bekannt und deshalb kann x kein Originalname gegeben werden. Bei dieser Art von Variablen geht es darum, beliebige Werte in Code umzuwandeln, d.h. bei der Auswertung von $(genSwap (4,5)) muss der Wert des Tupels (4,5) in eine Datenstruktur vom Typ Exp umgewandelt werden. T:swap (4,5) ⇒ App (VarE "T:swap") (TupE [LitE (IntPrimL 4), LitE (IntPrimL 5)]) Dies geschieht über die Funktion lift, aus der Typ-Klasse Lift. class Lift t where lift :: t -> ExpQ instance Lift Int where lift = litE . IntegerL . fromIntegral Die Instanzen der Klasse Lift erklären, wie die verschiedenen Typen der Klasse in den Typ Q Exp "gehoben" werden sollen. 13 genSwap :: (Int,Int) -> ExpQ genSwap x = do { t <- lift x ; return (AppE (VarE "T:swap") t) } Somit sind freie Variablen einer top-level Quasi Quotation statisch abgeschlossen. Sie brauchen in der Applikationsumgebung nicht verfügbar sein, oder können sogar durch gleiche Namen verdeckt sein. 4.3 Eigenschaften der Quotation Monade Die Quotation Monade übernimmt das Umbenennen der Variablen für den Programmierer. Die Namensgebung folgt einem einfachen Muster: • der Typname eines monadischen Datentyps setzt sich aus dem Namen des entsprechenden algebraischen Typnamen und einem Großbuchstaben (E für Expression und P für Pattern) zusammen. • die Namen der monadischen Datentypkonstruktoren beginnen mit einem Kleinbuchstaben, die der algebraischen mit einem Großbuchstaben. Als Beispiel seien hier die beiden Konstruktoren für die Lambda Abstraktion angegeben: LamE :: [Pat] -> Exp -> Exp -- { \ p1 p2 -> e } lamE :: [Pat] -> ExpQ -> ExpQ Man beachte, dass die Argumente von LamE lediglich Datentypen sind, wogegen die Argumente von lamE Berechnungen sind. lamE ps e = liftM (LamE ps) e Für Patterns (Pat) gibt es keinen entsprechenden monadischen Typ. Dies liegt daran, dass sich der Gültigkeitsbereich der Patterns über den ganzen Rumpf der Lambda Abstraktion erstreckt und es deshalb keinen Grund gibt, einen neuen Namen für sie zu generieren. Die Quotation Monade wird auch bei der Reification verwendet. Das Sprachkonstrukt der Reification gibt ein Ergebnis als Quotation Monade zurück. typeStruct gibt eine Berechnung vom Typ Q Dec zurück, die die interne Darstellung des GHC bezüglich des Datentyps T liefert: data T = A Int | B String typeStructT :: Q Dec typeStructT = reifyDecl T Die Berechnung von typeStructT ergibt folgende Ausgabe: Data [] "M:T" [] [Constr "TH_Bsp:A" [(NonStrict,Tcon (TconName "GHC.Base:Int"))], Constr "M:B" [(NonStrict,Tcon (TconName "GHC.Base:String"))]] [] 14 Das M vor dem Namen des Datentyps ist der Modulname, in dem der Datentyp deklariert ist. Der Datentyp Dec aus dem Modul THSyntax hat für Datentypdeklarationen die folgende Form: data Dec = … |DataD Cxt String [String] -- { data Cxt x => T x = A x | B (T x) [Con] [String] -- deriving (Z,W)} |… Reification kann beim Debugging sehr nützlich sein. Das Konstrukt reifyLocn gibt die Stelle seines Aufrufes innerhalb der Quelldatei in einen Q String zurück. Die Funktion assert führt eine Überprüfung durch und bricht gegebenenfalls mit einer Fehlermeldung ab: assert :: ExpQ -- Bool -> a -> a assert = [| \b r -> if b then r else (error ("Assertion failed at " ++ &reifyLocn |] Die Funktion assert kann fehlerhafte Zustände erkennen und teilt durch reifyLocn dem Benutzer mit, wo der Fehler abgefangen wurde. Hierbei ist es wichtig, das reifyLocn die Position seines "Splices" nicht etwa die seiner Definition. Es besteht die Möglichkeit, dass ein Meta-Programm fehlschlägt, weil es unachtsam verwendet wird. Solche Fehler liegen weniger am Programmierer als am Benutzer. Auf Grund der Quotation Monade kann der Programmierer solche Benutzerfehler abfangen und den Fehler rückmelden. Betrachten wir erneut die Funktion sel. Es würde keinen Sinn ergeben diese Funktion auf negative Parameter, etwa $(sel -1 3)anzuwenden. Wir benutzen den monadischen Standard-Operator fail: sel :: Int -> Int -> ExpQ sel i n |(i <= 0 || n <= 0) || (i >= n) = fail "Args are not valid to sel" | otherwise = sel i n = lamE [varP "x"]… Beliebige Input/Output Fähigkeiten für Meta-Programme können wir ebenfalls über die Monade erzielen. qIO :: IO a -> Q a Bei solchen I/O Funktionen ist allerdings Vorsicht geboten, da ein Programm bei Missbrauch dieser Funktionen beliebige Dateien löschen könnte. Oft möchte man ein generiertes Programm nicht direkt compilieren, sondern in einer Datei speichern. Gerade für einen guten Lerneffekt ist es von Vorteil, den Code, den man erzeugt hat, ansehen zu können. Zu diesem Zweck bieten die Bibliotheken, die Exp, Dec, u.a. zu Instanzen der Klasse Show machen. Dadurch können wir den im monadischen Gerüst berechneten Code ausgeben lassen. Zum Beispiel erhält man bei der Ausführung von 15 main = do { e <- runQ (cross2b) ; putStr (show e)} folgende Ausgabe: LamE [TupP [VarP "x", VarP "y"]] (TupE [AppE f (VarE "x"), AppE g (VarE "y")]) Die Quotation Monade besteht aus der IO Monade, die mit einer Umgebung Env erweitert ist. newtype Q a = Q (Env -> IO a) Die Umgebung beinhaltet folgende Informationen: • Eine veränderliche Speicherstelle, die der Generierung neuer Namen dient. • Die Ortsbestimmung eines (top-level) Splices, der eine Auswertung aufgerufen hat (z.B. für reifyLocn). • Die Symboltabelle des Compilers, um reifyDecl, reifyFixity und reifyType zu unterstützen. • Kommandozeilen-Optionen für reifyOpt. • 5 Template Haskell in der Anwendung 5.1 Expandieren von Deklarationen des FFI Um eine externe, fremde Bibliothek in Haskell zu importieren und nutzen zu können, gibt es das FFI (foreign function interface). Für einen Import muss man einige Anweisungen in verschiedene Dateien schreiben. Durch die Verwendung von Template Haskell ist es möglich, mit Hilfe der Syntax-Generatoren abstrakte Syntaxbäume dieser Importdeklarationen zu konstruieren. Diese Deklarationen der FFI foreign imports können zur Compile-Zeit in die Haskellmodule expandiert werden. Innerhalb des Splices können wir eine Liste von Bezeichnern angeben und bekommen die entsprechenden Importanweisungen zur CompileZeit geliefert. Im folgenden Beispielen aus [2] werden Graphikzeichen aus einer C Bibliothek importiert. $( let fs = ["acs_block","acs_hline","acs_ulcorner"] imp f = Foreign ( Import CCall Unsafe ("wrap.h "++ f) f (Tapp (Tcon (TconName "IO")) (Tcon (TconName "ChType"))) ) in return $(map imp fs) ) Das ist aber noch nicht alles, denn man kann in der Quotation Monade (vgl. Kapitel 4.3) beliebige IO-Aktionen mit der Funktion qIO ausführen, die C Quell- und Headerdateien 16 als Strings erzeugen, sie in Dateien schreiben und sogar den gcc innerhalb des Splices aufrufen: $( let fs = ["","",""] imp f = Foreign ( Import CCall Unsafe ("wrap.h "++ f) f (Tapp (Tcon (TconName "IO")) (Tcon (TconName "ChType"))) ) h f = "chtype " ++ f ++ "(void);\n" c f = "chtype " ++ f ++ "(void){\n" ++ " return " ++ map toUpper f ++ ";\n ++ "}\n" c_header = "#include \"wrap.h\"\n" h_header = "#include <curses.h>\n" in do qIO $(writeFile "wrap.h" $ h_header ++ concatMap h fs) qIO $(writeFile "wrap.c" $ c_header ++ concatMap c fs) qIO $(system "gcc -Wall -pedantic -c wrap.c -o wrap.o") return $(map imp fs) ) Die Funktion h erstellt die Strings für die Funktionsprototypen der C Headerdateien. Die Funktion c erstellt die Strings für C wrapper-Funktionen, die jede importierte Konstante in eine Funktion wandeln. Der Vorteil an diesem Ansatz mit Template Haskell ist es, dass Compilierung in einem Schritt erfolgt. Aus diesem Grund braucht man sich nicht um temporäre Dateien oder den Präprozessor zu kümmern, wo es zu Konflikten kommen kann. 5.2 Optimierung durch "Unrolling" Das "Unrolling" rekursiver Funktionen ist eine Technik zur Verbesserung der Leistung rekursiver Berechnungen. "Unrolling" reduziert den Kontrollflussaufwand eines Programms durch die Produktion von Code, der seltener die Abbruchbedingung einer Schleife überprüft. Das Ziel ist es, dem Programmierer eine Funktion zur Verfügung zu stellen, die die Definition einer Funktion als Argument erhält und eine äquivalente, aber "abgerollte", Funktion zurückliefert. Im folgenden Beispiel wird die Definition eines Mandelbrot-Visualisierers verwendet, der innerhalb eines "Splices" automatisch zur Compile-Zeit durch "Unrolling" optimiert wird. Hier wird die Quasi Quotation für Deklarationen verwendet, die einen abstrakten Syntaxbaum der Funktionsdeklarationen vom Typ Q [Dec] erzeugt. type Iterations = Int type T = Double type C = Complex T 17 type Colour = (Int, Int, Int) #ifdef TEMPLATE_HASKELL $( do [typ, func] <- [d| { #endif do_mb :: T -> Iterations -> C -> C -> Colour; do_mb k i z xy | i > 255 = (0, 0, 0) | otherwise = if (realPart z´)^2 + (imgPart z´)^2 > k^2 then (0, 0, i) else do_mb k (i+10) z´ xy where z´ = z^2 +xy; #ifdef TEMPLATE_HASKELL } |] let func´= unroll 30 1 (Integer 0) func return [ typ, func´ ] ) #endif Die eigentliche Berechnung des Visualisierers ist recht einfach und wird für jeden Punkt eines Bildes aufgerufen. Die #ifdef-Bereiche ermöglichen eine dynamische Fallunterscheidung, je nachdem, ob Template Haskell unterstützt wird, oder nicht. Die Funktion do_mb wird anfangs immer im Argument i mit Null aufgerufen und braucht nie mehr als 30 rekursive Aufrufe, um zu terminieren. Der Wert 30 wird der Funktion unroll übergeben und begrenzt die tiefe der "Auffaltung". Das Ergebnis der Auswertung ist eine Folge von verschachtelten bedingten Ausdrücken: do_mb :: T -> Iterations -> C -> C -> Colour; do_mb k 0 z xy = let z1 = z^2 + xy in if (realPart z1)^2 + (imgPart z1)^2 > k^2 then (0, 0, 0) else let z2 = z1^2 +xy in if ... ... else let z26 = z25^2 +xy in if (realPart z26)^2 + (imgPart z26)^2 > k^2 then (0, 0, 250) else (0, 0, 255) do_mb k i z xy | i > 255 = (0, 0, 0) 18 | otherwise = if (realPart z´)^2 + (imgPart z´)^2 > k^2 then (0, 0, i) else do_mb k (i+10) z´ xy where z´ = z^2 +xy; Die Details zur Funktionsweise des Codes würden den Rahmen dieser Arbeit sprengen, wichtig ist, dass die rekursiven Aufrufe durch Kopien des Funktionsrumpfes ersetzt werden. So wurden erstaunliche Effizienzgewinne erzielt, die bei einer Verlängerung der Compile-Zeit von 17 auf 29 Sekunden die Laufzeit von über einer Stunde auf fünf Minuten reduzierte. Interessierte seien hier auf die Arbeiten [2] und [5] verwiesen. Kapitel 6 Fazit Der Ansatz zur Compile-Zeit Meta-Programmierung mit Template Haskell ist seit Oktober 2002 in stetiger Entwicklung und repräsentiert eine nicht abgeschlossene Arbeit. Jedoch haben sich die Erweiterungen schon mehrfach als sehr nützlich erwiesen. In naher Zukunft werden wohl noch offene Designfragen geklärt und weitere Funktionalität hinzugefügt. Das Kernproblem, das nicht so leicht zu lösen sein wird, ist das der Unterstützung von Typund Striktheitsinformationen auf der Metaebene, die dem Programmierer für Applikationen nicht zur Verfügung stehen. Dadurch können Sicherheitsbedingungen für Transformationen auf abstrakten Syntaxbäumen nicht immer geprüft werden. Hier könnte die Einführung einer zusätzlichen Typdokumentation hilfreich sein, die einem Ausdruck vom Typ Exp Q in Form eines Parameters angehängt werden könnte. 19 Literatur- und Quellenverzeichnis [1] Sheard T., Peyton Jones, S.: Template Meta-programming for Haskell , to Haskell Workshop 2002. [2] Lynagh, I.: Template Haskell: A Report From The Field, Unpublished. [3] Sheard T., Peyton Jones, S.: Notes on Template Haskell Version 2, 7. November 2003. [4] GHC User's Guide: http://www.haskell.org/ghc/docs/latest/html/users_guide/templatehaskell.html, Januar 2004. [5] Lynagh, Ian: Unrolling and Simplifying Expressions with Template Haskell, web.comlab.ox.ac.uk/oucl/work/ian.lynagh/Fraskell/Template-Haskell-Utils.ps 20 Anhang A: Algebraische Datentyprepräsentation von Haskell data Exp = VarE String -- { x } | ConE String -- data T1 = C1 t1 t2; p = {C1} e1 e2 | LitE Lit -- { 5 or 'c'} | AppE Exp Exp -- { f x } | InfixE (Maybe Exp) Exp (Maybe Exp) -- {x + y} or {(x+)} or {(+ x)} or {(+)} -- It's a bit gruesome to use an Exp as the operator, but how else can we distinguish -- constructors from non-constructors? Maybe there should be a var-or-con type? -- Or maybe we should leave it to the String itself? | LamE [Pat] Exp -- { \ p1 p2 -> e } | TupE [Exp] -- { (e1,e2) } | CondE Exp Exp Exp -- { if e1 then e2 else e3 } | LetE [Dec] Exp -- { let x=e1; y=e2 in e3 } | CaseE Exp [Match] -- { case e of m1; m2 } | DoE [Stmt] -- { do { p <- e1; e2 } } | CompE [Stmt] -- { [ (x,y) | x <- xs, y <- ys ] } | ArithSeqE Range -- { [ 1 ,2 .. 10 ] } | ListE [ Exp ] -- { [1,2,3] } | SigE Exp Type -- e :: t | RecConE String [FieldExp] -- { T { x = y, z = w } } | RecUpdE Exp [FieldExp] -- { (f x) { z = w } } deriving( Show, Eq ) data Dec = FunD String [Clause] | ValD Pat Body [Dec] | DataD Cxt String [String] [Con] [String] | NewtypeD Cxt String [String] Con [String] | TySynD String [String] Type | ClassD Cxt String [String] [Dec] | InstanceD Cxt Type [Dec] | SigD String Type | ForeignD Foreign deriving( Show, Eq ) data Type = ForallT [String] Cxt Type | VarT String | ConT String | TupleT Int | ArrowT | ListT | AppT Type Type deriving( Show, Eq ) data Lit = CharL Char | StringL String | IntegerL Integer | RationalL Rational IntPrimL Integer | FloatPrimL Rational | DoublePrimL Rational deriving( Show, Eq ) -- { f p1 p2 = b where decs } -- { p = b where decs } -- { data Cxt x => T x = A x | B (T x) -- deriving (Z,W)} -- { newtype Cxt x => T x = A (B x) -- deriving (Z,W)} -- { type T x = (x,x) } -- { class Eq a => Ord a where ds } -- { instance Show w => Show [w] where ds } -- { length :: [a] -> Int } -- forall <vars>. <ctxt> -> <type> -- a -- T -- (,), (,,), etc. -- -> -- [] -- T a b -- Used for overloaded and non-overloadedliterals. -- We don't have a good way to represent non-- overloaded literals at the moment. Maybe that -- doesn't matter? -- Ditto data Pat = LitP Lit -- { 5 or 'c' } | VarP String -- { x } | TupP [Pat] -- { (p1,p2) } | ConP String [Pat] -- data T1 = C1 t1 t2; {C1 p1 p1} = e | TildeP Pat -- { ~p } | AsP String Pat -- { x @ p } | WildP -- { _ } | RecP String [FieldPat] -- f (Pt { pointx = x }) = g x | ListP [ Pat ] -- { [1,2,3] } deriving( Show, Eq ) Weiter Datentypen aus THSyntax: data Foreign, data Callconv, data Safety, data Strict, data Con, data Module. Anhang B: Monadische Syntax-Operatoren -- The quotation monad as IO fieldPat = (,) newtype Q a = Q (IO a) runQ :: Q a -> IO a runQ (Q x) = x type ExpQ = Q Exp type DecQ = Q Dec type ConQ = Q Con type TypeQ = Q Type type CxtQ = Q Cxt type MatchQ = Q Match type ClauseQ = Q Clause type BodyQ = Q Body type StmtQ = Q Stmt type RangeQ = Q Rang instance Monad Q where return x = Q (return x) (Q m) >>= k = Q (m >>= \r -> unQ (k r)) fail s = Q (fail s) instance MonadIO Q where liftIO = qIO -- Lowercase pattern syntax functions qIO :: IO a -> Q a qIO io = Q io intPrimL :: Integer -> Lit intPrimL = IntPrimL floatPrimL :: Rational -> Lit floatPrimL = FloatPrimL doublePrimL :: Rational -> Lit doublePrimL = DoublePrimL integerL :: Integer -> Lit integerL = IntegerL charL :: Char -> Lit charL = CharL stringL :: String -> Lit stringL = StringL rationalL :: Rational -> Lit rationalL = RationalL runQ :: Q a -> IO a runQ (Q io) = io gensym :: String -> Q String gensym s = Q ( do { n <- readIORef counter ; writeIORef counter (n+1) ; return(s++"'"++(show n)) }) class Lift t where lift :: t -> ExpQ instance Lift Integer where lift = litE . IntegerL litP :: Lit -> Pat litP = LitP varP :: String -> Pat varP = VarP tupP :: [Pat] -> Pat tupP = TupP conP :: String -> [Pat] -> Pat conP = ConP tildeP :: Pat -> Pat tildeP = TildeP asP :: String -> Pat -> Pat asP = AsP wildP :: Pat wildP = WildP recP :: String -> [FieldPat] -> Pat recP = RecP instance Lift Int where lift = litE . IntegerL . fromIntegral instance Lift Char where lift = litE . CharL instance Lift Bool where lift True = conE "GHC.Base:True" lift False = conE "GHC.Base:False" instance Lift a => Lift [a] where lift xs = listE (map lift xs) -- Combinator based types fieldPat :: String -> Pat -> (String, Pat) 22 listP :: [Pat] -> Pat listP = ListP -- Exp global :: String -> ExpQ global s = return (VarE s) varE :: String -> ExpQ varE s = return (VarE s) conE :: String -> ExpQ conE s = return (ConE s) litE :: Lit -> ExpQ litE c = return (LitE c) appE :: ExpQ -> ExpQ -> ExpQ appE x y = do { a <- x; b <- y; return (AppE a b)} infixE :: Maybe ExpQ -> ExpQ -> Maybe ExpQ -> ExpQ infixE (Just x) s (Just y) = do { a <- x; s' <- s; b <- y; return (InfixE (Just a) s' (Just b))} infixE Nothing s (Just y) = do { s' <- s; b <- y; return (InfixE Nothing s' (Just b))} infixE (Just x) s Nothing = do { a <- x; s' <- s; return (InfixE (Just a) s' Nothing)} infixE Nothing s Nothing = do { s' <- s; return (InfixE Nothing s' Nothing) } infixApp :: ExpQ -> ExpQ -> ExpQ -> ExpQ infixApp x y z = infixE (Just x) y (Just z) sectionL :: ExpQ -> ExpQ -> ExpQ sectionL x y = infixE (Just x) y Nothing sectionR :: ExpQ -> ExpQ -> ExpQ sectionR x y = infixE Nothing x (Just y) lamE :: [Pat] -> ExpQ -> ExpQ lamE ps e = liftM (LamE ps) e lam1E :: Pat -> ExpQ -> ExpQ lam1E p e = lamE [p] e -- Single-arg lambda tupE :: [ExpQ] -> ExpQ tupE es = do { es1 <- sequence es; return (TupE es1)} condE :: ExpQ -> ExpQ -> ExpQ -> ExpQ condE x y z = do { a <- x; b <- y; c <- z; return (CondE a b c)} letE :: [DecQ] -> ExpQ -> ExpQ letE ds e = do { ds2 <- sequence ds; e2 <- e; return (LetE ds2 e2) } caseE :: ExpQ -> [MatchQ] -> ExpQ caseE e ms = do { e1 <- e; ms1 <- sequence ms; return (CaseE e1 ms1) } doE :: [StmtQ] -> ExpQ doE ss = do { ss1 <- sequence ss; return (DoE ss1) } compE :: [StmtQ] -> ExpQ compE ss = do { ss1 <- sequence ss; return (CompE ss1) } … Weiter Hilfsfunktionen, Deklarationen, komplizierte Literale, implizite Parameter, u.a. wurden hier ausgelassen. 23