Haskell Language
Mall Haskell & QuasiQuotes
Sök…
Anmärkningar
Vad är mall Haskell?
Mall Haskell hänvisar till mallens metaprogrammeringsanläggningar inbyggda i GHC Haskell. Uppsatsen som beskriver den ursprungliga implementeringen kan hittas här .
Vad är etapper? (Eller, vad är scenbegränsningen?)
Stegen avser när kod körs. Normalt förväntas kod endast vid körning, men med mall Haskell kan kod köras vid kompileringstid. "Normal" -kod är steg 0 och kompileringstidskod är steg 1.
Stegbegränsningen hänvisar till det faktum att ett steg 0-program inte kan köras i steg 1 - detta skulle motsvara att kunna köra något vanligt program (inte bara metaprogram) vid sammanställningstiden.
Genom konvention (och för enkelhetens implementering) är koden i den aktuella modulen alltid steg 0 och kod som importeras från alla andra moduler är steg 1. Av detta skäl får endast uttryck från andra moduler skarvas.
Observera att ett steg 1-program är ett steg 0-uttryck av typ Q Exp
, Q Type
, etc; men konversationen är inte sant - inte alla värden (steg 0-program) av typ Q Exp
är ett steg 1-program,
Dessutom, eftersom splitsningar kan kapslas, kan identifierare ha steg som är större än 1. Scenbegränsningen kan sedan generaliseras - ett steg n- program kanske inte körs i något stadium m> n . Till exempel kan man se referenser till sådana steg större än 1 i vissa felmeddelanden:
>: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 |]
Om du använder mall Haskell orsakar inte-in-scope-fel från icke-relaterade identifierare?
Normalt kan alla deklarationer i en enda Haskell-modul ses som alla är ömsesidigt rekursiva. Med andra ord, varje toppdeklaration ligger inom varandras räckvidd i en enda modul. När mall Haskell är aktiverat ändras omfattningsreglerna - modulen bryts istället i grupper med kod separerade av TH-skarvar, och varje grupp är ömsesidigt rekursiv, och varje grupp omfattas av alla ytterligare grupper.
Q-typen
Konstruktören Q :: * -> *
definierad i Language.Haskell.TH.Syntax
är en abstrakt typ som representerar beräkningar som har åtkomst till kompileringstidsmiljön för modulen där beräkningen körs. Q
typen hanterar också variabel substituion, kallad namnupptagning av TH (och diskuteras här .) Alla skarvar har typ QX
för vissa X
Miljön för kompileringstid inkluderar:
- omfattande identifierare och information om nämnda identifierare,
- typer av funktioner
- typer och källdatatyper av konstruktörer
- full specifikation av typdeklarationer (klasser, typfamiljer)
- platsen i källkoden (rad, kolumn, modul, paket) där skarven inträffar
- funktionsfixiteter (GHC 7.10)
- aktiverade GHC-tillägg (GHC 8.0)
Q
typen har också förmågan att generera nya namn, med funktionen newName :: String -> Q Name
. Observera att namnet inte är bundet någonstans implicit, så användaren måste binda det själva, och så att se till att den resulterande användningen av namnet är välskalad är användarens ansvar.
Q
har instanser för Functor,Monad,Applicative
och detta är det huvudsakliga gränssnittet för att manipulera Q
värden, tillsammans med kombinationerna som tillhandahålls i Language.Haskell.TH.Lib
, som definierar en hjälpfunktion för varje konstruktör av TH ast i formen:
LitE :: Lit -> Exp
litE :: Lit -> ExpQ
AppE :: Exp -> Exp -> Exp
appE :: ExpQ -> ExpQ -> ExpQ
Observera att ExpQ
, TypeQ
, DecsQ
och PatQ
är synonymer för AST-typerna som vanligtvis lagras i Q
typen.
TH-biblioteket tillhandahåller en funktion runQ :: Quasi m => Q a -> ma
, och det finns ett exempel Quasi IO
, så det verkar som om Q
typen bara är en fin IO
. Användningen av runQ :: Q a -> IO a
producerar emellertid en IO
åtgärd som inte har tillgång till någon kompileringstidsmiljö - detta är endast tillgängligt i den faktiska Q
typen. Sådana IO
åtgärder kommer att misslyckas vid körning om man försöker komma åt nämnda miljö.
En n-arity curry
Det bekanta
curry :: ((a,b) -> c) -> a -> b -> c
curry = \f a b -> f (a,b)
funktion kan generaliseras till tupler av godtycklig arity, till exempel:
curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d
curry4 :: ((a, b, c, d) -> e) -> a -> b -> c -> d -> e
Men att skriva sådana funktioner för tuples av arity 2 till (t.ex.) 20 för hand skulle vara tråkigt (och bortse från det faktum att närvaron av 20 tuples i ditt program nästan säkert signaliserar designproblem som bör fixas med poster).
Vi kan använda Template Haskell för att producera sådana curryN
funktioner för godtyckliga n
:
{-# LANGUAGE TemplateHaskell #-}
import Control.Monad (replicateM)
import Language.Haskell.TH (ExpQ, newName, Exp(..), Pat(..))
import Numeric.Natural (Natural)
curryN :: Natural -> Q Exp
curryN
funktionen tar ett naturligt tal och producerar curryfunktionen för denna arity, som en Haskell AST.
curryN n = do
f <- newName "f"
xs <- replicateM (fromIntegral n) (newName "x")
Först producerar vi färskt variabler för vart och ett av argumenten för funktionen - en för ingångsfunktionen och en för var och en av argumenten till nämnda funktion.
let args = map VarP (f:xs)
Uttrycket args
representerar mönstret f x1 x2 .. xn
. Observera att ett mönster är en separat syntaktisk enhet - vi kan ta samma mönster och placera det i en lambda, eller en funktionsbindning, eller till och med LHS för en låtbindning (vilket skulle vara ett fel).
ntup = TupE (map VarE xs)
Funktionen måste bygga argumenttupeln från sekvensen av argument, vilket är vad vi har gjort här. Notera skillnaden mellan mönstervariabler ( VarP
) och uttrycksvariabler ( VarE
).
return $ LamE args (AppE (VarE f) ntup)
Slutligen är värdet som vi producerar AST \f x1 x2 .. xn -> f (x1, x2, .. , xn)
.
Vi kunde också ha skrivit den här funktionen med offert och "lyfta" konstruktörer:
...
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)) |]
Observera att offerten måste vara syntaktiskt giltiga, så [| \ $(map varP (f:xs)) -> .. |]
är ogiltigt, eftersom det inte finns något sätt i vanliga Haskell att förklara en 'lista' med mönster - ovanstående tolkas som \ var -> ..
och splitsat uttryck förväntas ha typ PatQ
, dvs ett enda mönster, inte en lista med mönster.
Slutligen kan vi ladda denna TH-funktion i GHCi:
>: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
Detta exempel anpassas främst härifrån .
Syntax av mall Haskell och kvasikvoter
Mall Haskell aktiveras av förlängningen -XTemplateHaskell
GHC. Denna utvidgning möjliggör alla syntaktiska funktioner som är mer detaljerade i detta avsnitt. Den fullständiga informationen om mall Haskell ges av användarhandboken .
skarvar
En splice är en ny syntaktisk enhet som aktiveras av Mall Haskell, skriven som
$(...)
, där(...)
är ett uttryck.Det får inte finnas ett avstånd mellan
$
och uttryckets första karaktär; och Mall Haskell åsidosätter analysen av$
-operatören - t exf$g
tolkas normalt som($) fg
medan med Mall Haskell aktiverat, analyseras det som en skiva.När en skarv visas på den översta nivån kan
$
utelämnas. I detta fall är det skarvade uttrycket hela linjen.En splits representerar kod som körs vid kompileringstiden för att producera en Haskell AST, och som AST kompileras som Haskell-kod och infogas i programmet
Skarvar kan visas i stället för: uttryck, mönster, typer och toppdeklarationer. Typen av det skarvade uttrycket, i båda fallen, är
Q Exp
,Q Pat
,Q Type
,Q [Decl]
. Observera att deklarations skarvar endast kan visas på den översta nivån, medan andra kan vara inuti andra uttryck, mönster eller typer, respektive.
Uttryckskvoteringar (Obs: inte en QuasiQuotation)
Ett uttryckskvot är en ny syntaktisk enhet skriven som en av:
-
[e|..|]
eller[|..|]
-..
är ett uttryck och citatet har typQ Exp
; -
[p|..|]
-..
är ett mönster och citatet har typQ Pat
; -
[t|..|]
-..
är en typ och offerten har typQ Type
; -
[d|..|]
-..
är en lista med deklarationer och offerten har typQ [Dec]
.
-
Ett uttryckskvotering tar ett sammanställningstidsprogram och producerar den AST som representeras av det programmet.
Användningen av ett värde i en offert (t.ex.
\x -> [| x |]
) utan en skarv motsvarar syntaktiskt socker för\x -> [| $(lift x) |]
, därlift :: Lift t => t -> Q Exp
kommer från klassen
class Lift t where lift :: t -> Q Exp default lift :: Data t => t -> Q Exp
Typade skarvar och offert
Typade skarvar är i likhet med tidigare nämnda (otypade) skarvar och skrivs som
$$(..)
där(..)
är ett uttryck.Om
e
har typQ (TExp a)
så har$$e
typa
.Typerade citat har formen
[||..||]
där..
är ett uttryck av typa
; den resulterande offerten har typQ (TExp a)
.Typat uttryck kan konverteras till otypade:
unType :: TExp a -> Exp
.
QuasiQuotes
QuasiQuotes generaliserar uttryckskvoteringar - tidigare har den parser som används av uttryckscitationsteckningen en av en fast uppsättning (
e,p,t,d
), men QuasiQuotes tillåter en anpassad parser att definieras och användas för att producera kod vid kompileringstid. Kvasitoteringar kan visas i alla samma sammanhang som vanliga citat.Ett kvasi-offert skrivs som
[iden|...|]
, däriden
är en identifierare av typenLanguage.Haskell.TH.Quote.QuasiQuoter
.En
QuasiQuoter
består helt enkelt av fyra tolkare, en för var och en av de olika sammanhang där citat kan visas:
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp, quotePat :: String -> Q Pat, quoteType :: String -> Q Type, quoteDec :: String -> Q [Dec] }
namn
Haskell-identifierare representeras av typen
Language.Haskell.TH.Syntax.Name
. Namnen bildar bladen på abstrakta syntaxträd som representerar Haskell-program i Mall Haskell.En identifierare som för närvarande är inom räckvidd kan förvandlas till ett namn med antingen:
'e
eller'T
I det första fallet,e
tolkas i expressions omfattning, medan i det andra falletT
är i typen omfattning (som erinrar om att typer och värde konstruktörer kan dela namnet utan amiguity i Haskell).