Haskell Language
Vorlage Haskell & QuasiQuotes
Suche…
Bemerkungen
Was ist Template Haskell?
Template Haskell bezieht sich auf die in GHC Haskell integrierten Template-Metaprogrammierungsfunktionen. Das Papier der ursprüngliche Implementierung der Beschreibung gefunden werden kann hier .
Was sind Stufen? (Oder was ist die Bühneneinschränkung?)
Stufen beziehen sie auf , wenn der Code ausgeführt wird. Normalerweise wird Code nur zur Laufzeit ausgeführt, aber mit Template Haskell kann Code zur Kompilierzeit ausgeführt werden. "Normal" -Code ist Stufe 0 und der Kompilierzeitcode ist Stufe 1.
Die Einschränkung der Stufe bezieht sich auf die Tatsache, dass ein Stufe 0-Programm möglicherweise nicht auf Stufe 1 ausgeführt werden kann. Dies wäre gleichbedeutend mit der Möglichkeit, ein normales Programm (nicht nur ein Meta-Programm) zur Kompilierzeit auszuführen.
Konventionell (und der Einfachheit der Implementierung halber) ist Code im aktuellen Modul immer Stufe 0 und Code, der von allen anderen Modulen importiert wird, Stufe 1. Aus diesem Grund dürfen nur Ausdrücke aus anderen Modulen gespleißt werden.
Beachten Sie, dass ein Programm der Stufe 1 ein Ausdruck der Stufe 0 des Typs Q Exp
, Q Type
usw. ist. aber das Gegenteil ist nicht wahr - nicht jeder Wert (Programm der Stufe 0) vom Typ Q Exp
ist ein Programm der Stufe 1,
Da Spleiße verschachtelt werden können, können Identifizierer außerdem Stufen haben, die größer als 1 sind. Die Stufenbeschränkung kann dann verallgemeinert werden - ein Programm der Stufe n kann in keiner Stufe m> n ausgeführt werden . Beispielsweise können in bestimmten Fehlermeldungen Verweise auf solche Stufen größer als 1 angezeigt werden:
>:t [| \x -> $x |]
<interactive>:1:10: error:
* Stage error: `x' is bound at stage 2 but used at stage 1
* In the untyped splice: $x
In the Template Haskell quotation [| \ x -> $x |]
Die Verwendung von Template Haskell führt zu Fehlern außerhalb des Gültigkeitsbereichs von nicht verknüpften Bezeichnern.
Normalerweise können alle Deklarationen in einem einzigen Haskell-Modul als gegenseitig rekursiv betrachtet werden. Mit anderen Worten, jede Deklaration auf oberster Ebene ist in einem einzigen Modul im Umfang jeder anderen. Wenn Template Haskell aktiviert ist, ändern sich die Bereichsregeln. Stattdessen wird das Modul in durch TH-Spleiße getrennte Codegruppen aufgeteilt, und jede Gruppe ist wechselseitig rekursiv. Jede Gruppe befindet sich im Bereich aller weiteren Gruppen.
Der Q-Typ
Der in Language.Haskell.TH.Syntax
definierte Konstruktor Q :: * -> *
stellt einen abstrakten Typ dar, der Berechnungen darstellt, die Zugriff auf die Kompilierzeitumgebung des Moduls haben, in dem die Berechnung ausgeführt wird. Der Q
Typ behandelt auch die variable Substitution, die als Name Capture durch TH bezeichnet wird (und hier erläutert wird). Alle Spleiße haben für einige X
Typ QX
.
Die Umgebung zur Kompilierungszeit umfasst:
- Kennungen und Informationen zu diesen Kennungen,
- Arten von Funktionen
- Typen und Quelldatentypen von Konstruktoren
- vollständige Spezifikation der Typdeklarationen (Klassen, Typfamilien)
- Die Position im Quellcode (Zeile, Spalte, Modul, Paket), an der der Splice erfolgt
- Fixitäten von Funktionen (GHC 7.10)
- aktivierte GHC-Erweiterungen (GHC 8.0)
Der Q
Typ kann auch neue Namen mit der Funktion newName :: String -> Q Name
generieren. Beachten Sie, dass der Name nicht irgendwo implizit gebunden ist. Daher muss der Benutzer ihn selbst binden. Daher muss der Benutzer dafür verantwortlich sein, dass der Name gut verwendet wird.
Q
hat Instanzen für Functor,Monad,Applicative
Dies ist die Hauptschnittstelle für die Bearbeitung von Q
Werten, zusammen mit den in Language.Haskell.TH.Lib
bereitgestellten Kombinatoren, die für jeden Konstruktor des TH ast des Formulars eine Hilfsfunktion definieren:
LitE :: Lit -> Exp
litE :: Lit -> ExpQ
AppE :: Exp -> Exp -> Exp
appE :: ExpQ -> ExpQ -> ExpQ
Beachten Sie, dass ExpQ
, TypeQ
, DecsQ
und PatQ
Synonyme für die AST-Typen sind, die normalerweise im Q
Typ gespeichert werden.
Die TH-Bibliothek stellt eine Funktion runQ :: Quasi m => Q a -> ma
, und es gibt eine Instanz Quasi IO
, es scheint also, dass der Q
Typ nur eine schicke IO
. Jedoch ist die Verwendung von runQ :: Q a -> IO a
eine produziert IO
Aktion , die keinen Zugang zu Kompilierung-Umgebung hat - dies ist nur in dem tatsächlich verfügbaren Q
- Typ. Solche IO
Aktionen schlagen zur Laufzeit fehl, wenn versucht wird, auf die Umgebung zuzugreifen.
Ein n-arity Curry
Das vertraute
curry :: ((a,b) -> c) -> a -> b -> c
curry = \f a b -> f (a,b)
Funktion kann auf Tupel beliebiger Arität verallgemeinert werden, zum Beispiel:
curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d
curry4 :: ((a, b, c, d) -> e) -> a -> b -> c -> d -> e
Das Schreiben solcher Funktionen für Tupel der Arity 2 bis (zB) 20 von Hand wäre jedoch langwierig (und die Tatsache zu ignorieren, dass das Vorhandensein von 20 Tupeln in Ihrem Programm fast sicher Designprobleme signalisiert, die mit Datensätzen behoben werden sollten).
Wir können Template Haskell verwenden, um solche curryN
Funktionen für beliebige n
zu erzeugen:
{-# LANGUAGE TemplateHaskell #-}
import Control.Monad (replicateM)
import Language.Haskell.TH (ExpQ, newName, Exp(..), Pat(..))
import Numeric.Natural (Natural)
curryN :: Natural -> Q Exp
Die curryN
Funktion nimmt eine natürliche Zahl an und erzeugt die Curry-Funktion dieser Arität als Haskell-AST.
curryN n = do
f <- newName "f"
xs <- replicateM (fromIntegral n) (newName "x")
Zuerst erzeugen wir frische Typvariablen für jedes der Argumente der Funktion - eine für die Eingabefunktion und eine für jedes der Argumente für diese Funktion.
let args = map VarP (f:xs)
Der Ausdruck args
repräsentiert das Muster f x1 x2 .. xn
. Beachten Sie, dass ein Muster eine separate syntaktische Entität ist - wir könnten dasselbe Muster in ein Lambda oder eine Funktionsbindung oder sogar die LHS einer Let-Bindung (die ein Fehler wäre) platzieren.
ntup = TupE (map VarE xs)
Die Funktion muss das Argument-Tupel aus der Folge von Argumenten erstellen, wie wir es hier gemacht haben. Beachten Sie den Unterschied zwischen Mustervariablen ( VarP
) und Ausdrucksvariablen ( VarE
).
return $ LamE args (AppE (VarE f) ntup)
Schließlich ist der Wert, den wir erzeugen, AST \f x1 x2 .. xn -> f (x1, x2, .. , xn)
.
Wir hätten diese Funktion auch mit Zitaten und "angehobenen" Konstruktoren schreiben können:
...
import Language.Haskell.TH.Lib
curryN' :: Natural -> ExpQ
curryN' n = do
f <- newName "f"
xs <- replicateM (fromIntegral n) (newName "x")
lamE (map varP (f:xs))
[| $(varE f) $(tupE (map varE xs)) |]
Beachten Sie, dass Zitate syntaktisch gültig sein müssen, also [| \ $(map varP (f:xs)) -> .. |]
ist ungültig, da es im regulären Haskell nicht möglich ist, eine 'Liste' von Mustern zu deklarieren - das obige wird als \ var -> ..
und das interpretiert Es wird erwartet, dass der gespleißte Ausdruck den Typ PatQ
, dh ein einzelnes Muster, keine Liste von Mustern.
Zum Schluss können wir diese TH-Funktion in GHCi laden:
>:set -XTemplateHaskell
>:t $(curryN 5)
$(curryN 5)
:: ((t1, t2, t3, t4, t5) -> t) -> t1 -> t2 -> t3 -> t4 -> t5 -> t
>$(curryN 5) (\(a,b,c,d,e) -> a+b+c+d+e) 1 2 3 4 5
15
Dieses Beispiel wird hauptsächlich von hier aus angepasst.
Syntax von Template Haskell und Quasiquotes
Template Haskell wird von der GHC-Erweiterung -XTemplateHaskell
. Diese Erweiterung ermöglicht alle in diesem Abschnitt ausführlicher beschriebenen syntaktischen Funktionen. Die vollständigen Details zu Template Haskell finden Sie in der Bedienungsanleitung .
Spleiße
Ein Splice ist eine neue syntaktische Entität, die von Template Haskell aktiviert wird und als
$(...)
, wobei(...)
ein Ausdruck ist.Zwischen
$
und dem ersten Zeichen des Ausdrucks darf kein Leerzeichen stehen. und Template Haskell überschreibt die Analyse des$
-Operators - z. B. wirdf$g
normalerweise als($) fg
analysiert, wohingegen bei aktiviertem Template Haskell ein Splice analysiert wird.Wenn auf der obersten Ebene ein Spleiß erscheint, kann das
$
weggelassen werden. In diesem Fall ist der gespleißte Ausdruck die gesamte Zeile.Ein Splice stellt Code dar, der zur Kompilierzeit ausgeführt wird, um einen Haskell-AST zu erzeugen, und dieser AST wird als Haskell-Code kompiliert und in das Programm eingefügt
Spleiße können anstelle von Ausdrücken, Mustern, Typen und Deklarationen der obersten Ebene angezeigt werden. Der Typ des gespleißten Ausdrucks ist jeweils
Q Exp
,Q Pat
,Q Type
,Q [Decl]
. Beachten Sie, dass Deklarationsspleiße möglicherweise nur auf der obersten Ebene angezeigt werden, während sich die anderen in anderen Ausdrücken, Mustern oder Typen befinden können.
Ausdruckszitate (Hinweis: kein QuasiQuotation)
Ein Ausdruckszitat ist eine neue syntaktische Entität, geschrieben als eines der folgenden Elemente:
-
[e|..|]
oder[|..|]
-..
ist ein Ausdruck und das Zitat hat den TypQ Exp
; -
[p|..|]
-..
ist ein Muster und das Zitat hat den TypQ Pat
; -
[t|..|]
-..
ist ein Typ und das Angebot hat den TypQ Type
; -
[d|..|]
-..
ist eine Liste von Deklarationen und das Zitat hat den TypQ [Dec]
.
-
Ein Ausdruckszitat benötigt ein Kompilierzeitprogramm und erstellt die von diesem Programm dargestellte AST.
Die Verwendung eines Werts in einem Zitat (z. B.
\x -> [| x |]
) ohne Spleiß entspricht dem syntaktischen Zucker für\x -> [| $(lift x) |]
, wobeilift :: Lift t => t -> Q Exp
von der Klasse kommt
class Lift t where lift :: t -> Q Exp default lift :: Data t => t -> Q Exp
Typisierte Spleiße und Zitate
Typisierte Spleiße ähneln den zuvor erwähnten (nicht typisierten) Spleißen und werden als
$$(..)
wobei(..)
ein Ausdruck ist.Wenn
e
den TypQ (TExp a)
hat, hat$$e
den Typa
.Typisierte Zitate haben die Form
[||..||]
wobei..
ein Ausdruck vom Typa
; Das resultierende Zitat hat den TypQ (TExp a)
.Typisierte Ausdrücke können in nicht typisierte umgewandelt werden:
unType :: TExp a -> Exp
.
QuasiQuotes
QuasiQuotes verallgemeinern Ausdruckszitate - der Parser, der im Ausdruckszitat verwendet wird, ist früher einer festen Menge (
e,p,t,d
), aber QuasiQuotes ermöglicht die Definition und die Erstellung eines benutzerdefinierten Parsers zur Kompilierzeit. Quasi-Zitate können im selben Kontext wie reguläre Zitate erscheinen.Ein Quasi-Zitat wird als
[iden|...|]
, wobeiiden
eine Kennung vom TypLanguage.Haskell.TH.Quote.QuasiQuoter
.Ein
QuasiQuoter
besteht einfach aus vier Parsern, einen für jeden der verschiedenen Kontexte, in denen Zitate auftreten können:
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp, quotePat :: String -> Q Pat, quoteType :: String -> Q Type, quoteDec :: String -> Q [Dec] }
Namen
Haskell-Bezeichner werden durch den Typ
Language.Haskell.TH.Syntax.Name
. Namen bilden die Blätter abstrakter Syntaxbäume, die Haskell-Programme in Template Haskell darstellen.Eine Kennung, die sich aktuell im Gültigkeitsbereich befindet, kann in einen Namen umgewandelt werden mit:
'e
oder'T
Im ersten Fall wirde
im Ausdrucksbereich interpretiert, während im zweiten FallT
im Typenbereich liegt (unter Hinweis darauf, dass Typen und Wertkonstruktoren den Namen ohne Übereinstimmung in Haskell teilen können).