Haskell Language
Template Haskell e QuasiQuotes
Ricerca…
Osservazioni
Che cos'è Template Haskell?
Template Haskell si riferisce alle strutture di meta-programmazione template incorporate in GHC Haskell. Il documento che descrive l'implementazione originale può essere trovato qui .
Quali sono le fasi? (Oppure, qual è la restrizione di scena?)
Le fasi si riferiscono a quando il codice viene eseguito. Normalmente, il codice è indicato solo in fase di esecuzione, ma con Template Haskell, il codice può essere eseguito in fase di compilazione. Il codice "normale" è lo stadio 0 e il codice di compilazione è lo stadio 1.
La limitazione dello stage si riferisce al fatto che un programma stage 0 non può essere eseguito nella fase 1 - questo sarebbe equivalente a essere in grado di eseguire qualsiasi programma regolare (non solo meta-programma) in fase di compilazione.
Per convenzione (e per semplicità di implementazione), il codice nel modulo corrente è sempre lo stadio 0 e il codice importato da tutti gli altri moduli è lo stadio 1. Per questo motivo, solo le espressioni di altri moduli possono essere giuntate.
Si noti che un programma stage 1 è un'espressione stage 0 di tipo Q Exp
, Q Type
, ecc; ma il contrario non è vero - non ogni valore (programma stage 0) di tipo Q Exp
è un programma stage 1,
Inoltre, poiché le giunzioni possono essere annidate, gli identificatori possono avere fasi superiori a 1. La restrizione di fase può quindi essere generalizzata - un programma di stage n non può essere eseguito in nessuno stadio m> n . Ad esempio, in certi messaggi di errore si possono vedere riferimenti a tali fasi superiori a 1:
>: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 |]
L'uso di Template Haskell causa errori non in ambito da identificatori non correlati?
Normalmente, tutte le dichiarazioni in un singolo modulo Haskell possono essere pensate come tutte reciprocamente ricorsive. In altre parole, ogni dichiarazione di livello superiore è nell'ambito di ogni altro in un singolo modulo. Quando Template Haskell è abilitato, le regole di scoping cambiano - il modulo viene invece suddiviso in gruppi di codice separati dalle giunzioni TH, e ciascun gruppo è reciprocamente ricorsivo e ciascun gruppo nell'ambito di tutti gli altri gruppi.
Il tipo Q
Il costruttore di tipi Q :: * -> *
definito in Language.Haskell.TH.Syntax
è un tipo astratto che rappresenta calcoli che hanno accesso all'ambiente di compilazione del modulo in cui viene eseguito il calcolo. Il tipo Q
gestisce anche la sostituzione parziale, chiamata acquisizione del nome da TH (e discussa qui ). Tutte le giunzioni hanno tipo QX
per alcuni X
L'ambiente di compilazione include:
- identificatori e informazioni in-scope su detti identificatori,
- tipi di funzioni
- tipi e tipi di dati di origine dei costruttori
- specifica completa delle dichiarazioni di tipo (classi, famiglie di tipi)
- la posizione nel codice sorgente (riga, colonna, modulo, pacchetto) in cui si verifica la giunzione
- fissità delle funzioni (GHC 7.10)
- estensioni GHC abilitate (GHC 8.0)
Il tipo Q
ha anche la capacità di generare nuovi nomi, con la funzione newName :: String -> Q Name
. Si noti che il nome non è associato in alcun modo in modo implicito, quindi l'utente deve collegarlo da sé e pertanto assicurarsi che l'utilizzo risultante del nome sia ben definito è responsabilità dell'utente.
Q
ha istanze per Functor,Monad,Applicative
e questa è l'interfaccia principale per la manipolazione dei valori Q
, insieme ai combinatori forniti in Language.Haskell.TH.Lib
, che definiscono una funzione di supporto per ogni costruttore del TH asto della forma:
LitE :: Lit -> Exp
litE :: Lit -> ExpQ
AppE :: Exp -> Exp -> Exp
appE :: ExpQ -> ExpQ -> ExpQ
Nota che ExpQ
, TypeQ
, DecsQ
e PatQ
sono sinonimi per i tipi di AST che sono tipicamente memorizzati all'interno del tipo Q
La libreria TH fornisce una funzione runQ :: Quasi m => Q a -> ma
, e c'è un'istanza Quasi IO
, quindi sembrerebbe che il tipo Q
sia solo un IO
fantasioso. Tuttavia, l'uso di runQ :: Q a -> IO a
produce un'azione IO
che non ha accesso ad alcun ambiente di compilazione - questo è disponibile solo nel tipo Q
effettivo. Tali azioni IO
falliranno in fase di esecuzione se si tenta di accedere a detto ambiente.
Un curry n-ario
Il familiare
curry :: ((a,b) -> c) -> a -> b -> c
curry = \f a b -> f (a,b)
la funzione può essere generalizzata a tuple di arbitrarietà, ad esempio:
curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d
curry4 :: ((a, b, c, d) -> e) -> a -> b -> c -> d -> e
Tuttavia, scrivere tali funzioni per le tuple di arity 2 per (es.) 20 a mano sarebbe noioso (ignorando il fatto che la presenza di 20 tuple nel programma quasi certamente segnala problemi di progettazione che dovrebbero essere corretti con i record).
Possiamo usare Template Haskell per produrre tali funzioni curryN
per n
arbitrario:
{-# LANGUAGE TemplateHaskell #-}
import Control.Monad (replicateM)
import Language.Haskell.TH (ExpQ, newName, Exp(..), Pat(..))
import Numeric.Natural (Natural)
curryN :: Natural -> Q Exp
La funzione curryN
prende un numero naturale e produce la funzione curry di quell'arity, come Haskell AST.
curryN n = do
f <- newName "f"
xs <- replicateM (fromIntegral n) (newName "x")
Innanzitutto produciamo nuove variabili di tipo per ciascuno degli argomenti della funzione: uno per la funzione di input e uno per ciascuno degli argomenti di detta funzione.
let args = map VarP (f:xs)
L'espressione args
rappresenta il modello f x1 x2 .. xn
. Nota che un pattern è un'entità sintattica separata: potremmo prendere lo stesso pattern e posizionarlo in un lambda, o in un binding di funzione, o anche nel LHS di un let binding (che sarebbe un errore).
ntup = TupE (map VarE xs)
La funzione deve costruire l'argomento tuple dalla sequenza di argomenti, che è ciò che abbiamo fatto qui. Notare la distinzione tra variabili di pattern ( VarP
) e variabili di espressione ( VarE
).
return $ LamE args (AppE (VarE f) ntup)
Infine, il valore che produciamo è AST \f x1 x2 .. xn -> f (x1, x2, .. , xn)
.
Avremmo potuto anche scrivere questa funzione usando citazioni e costruttori "sollevati":
...
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)) |]
Si noti che le quotazioni devono essere sintatticamente valide, quindi [| \ $(map varP (f:xs)) -> .. |]
non è valido, perché in Haskell non esiste alcun modo per dichiarare un 'elenco' di pattern - il precedente è interpretato come \ var -> ..
e il si prevede che l'espressione spliced abbia il tipo PatQ
, cioè un singolo pattern, non un elenco di pattern.
Infine, possiamo caricare questa funzione TH in 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
Questo esempio è adattato principalmente da qui .
Sintassi di Template Haskell e Quasiquotes
Template Haskell è abilitato -XTemplateHaskell
GHC. Questa estensione abilita tutte le caratteristiche sintattiche ulteriormente descritte in questa sezione. I dettagli completi su Template Haskell sono forniti dalla guida dell'utente .
giunzioni
Una giuntura è una nuova entità sintattica attivata da Template Haskell, scritta come
$(...)
, dove(...)
è una certa espressione.Non deve esserci uno spazio tra
$
e il primo carattere dell'espressione; e Template Haskell sovrascrive l'analisi dell'operatore$
- ad esempiof$g
viene normalmente analizzato come($) fg
mentre con Template Haskell abilitato, viene analizzato come una giunzione.Quando una giunzione appare al livello più alto,
$
può essere omesso. In questo caso, l'espressione splicing è l'intera linea.Una splice rappresenta un codice che viene eseguito in fase di compilazione per produrre un AST Haskell e che AST è compilato come codice Haskell e inserito nel programma
Le giunzioni possono apparire al posto di: espressioni, modelli, tipi e dichiarazioni di livello superiore. Il tipo dell'espressione giuntata, rispettivamente, in entrambi i casi è
Q Exp
,Q Pat
,Q Type
,Q [Decl]
. Nota che le giunzioni di dichiarazione possono apparire solo al livello superiore, mentre le altre possono essere all'interno di altre espressioni, modelli o tipi, rispettivamente.
Citazioni sulle espressioni (nota: non una QuasiQuotation)
Una citazione di espressione è una nuova entità sintattica scritta come una delle seguenti:
-
[e|..|]
o[|..|]
-..
è un'espressione e la quotazione ha tipoQ Exp
; -
[p|..|]
-..
è un modello e la quotazione ha tipoQ Pat
; -
[t|..|]
-..
è un tipo e la quotazione ha tipoQ Type
; -
[d|..|]
-..
è una lista di dichiarazioni e la quotazione ha tipoQ [Dec]
.
-
Una citazione di espressioni richiede un programma a tempo compilato e produce l'AST rappresentato da quel programma.
L'uso di un valore in una citazione (ad es.
\x -> [| x |]
) senza una splice corrisponde allo zucchero sintattico per\x -> [| $(lift x) |]
, dovelift :: Lift t => t -> Q Exp
viene dalla classe
class Lift t where lift :: t -> Q Exp default lift :: Data t => t -> Q Exp
Giunzioni e citazioni tipizzate
Le giunzioni tipizzate sono simili alle giunzioni (non tipizzate) precedentemente menzionate e sono scritte come
$$(..)
dove(..)
è un'espressione.Se
e
ha tipoQ (TExp a)
allora$$e
ha tipoa
.Le virgolette tipizzate assumono la forma
[||..||]
dove..
è un'espressione di tipoa
; la citazione risultante ha tipoQ (TExp a)
.L'espressione tipizzata può essere convertita in una non tipizzata:
unType :: TExp a -> Exp
.
QuasiQuotes
QuasiQuotes generalizza le citazioni di espressioni: in precedenza, il parser utilizzato dall'espressione quotation è uno di un set fisso (
e,p,t,d
), ma QuasiQuotes consente di definire e utilizzare un parser personalizzato per produrre codice in fase di compilazione. Le quasi-quotazioni possono apparire in tutti gli stessi contesti delle quotazioni regolari.Una quasi-citazione è scritta come
[iden|...|]
, doveiden
è un identificatore di tipoLanguage.Haskell.TH.Quote.QuasiQuoter
.Un
QuasiQuoter
è composto semplicemente da quattro parser, uno per ciascuno dei diversi contesti in cui possono comparire le citazioni:
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp, quotePat :: String -> Q Pat, quoteType :: String -> Q Type, quoteDec :: String -> Q [Dec] }
nomi
Gli identificatori di Haskell sono rappresentati dal tipo
Language.Haskell.TH.Syntax.Name
. I nomi formano le foglie di alberi di sintassi astratti che rappresentano i programmi Haskell in Template Haskell.Un identificatore attualmente in ambito può essere trasformato in un nome con:
'e
o'T
Nel primo caso,e
viene interpretato nello scope di espressione, mentre nel secondo casoT
è nel tipo scope (ricordando che i costruttori di tipi e valori possono condividere il nome senza ambiguità in Haskell).