Generische funktionale Programmierung 2 Template Haskell

Werbung
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
Herunterladen