Haskell Language
Szablon Haskell i QuasiQuotes
Szukaj…
Uwagi
Co to jest szablon Haskell?
Szablon Haskell odnosi się do narzędzi do metaprogramowania szablonów wbudowanych w GHC Haskell. Artykuł opisujący oryginalne wdrożenie można znaleźć tutaj .
Jakie są etapy? (Lub jakie jest ograniczenie etapowe?)
Etapy patrz, gdy kod jest wykonywany. Zwykle kod jest wykonywany tylko w czasie wykonywania, ale dzięki szablonowi Haskell kod można wykonać w czasie kompilacji. Kod „normalny” to etap 0, a kod czasu kompilacji to etap 1.
Ograniczenie etapu odnosi się do faktu, że program etapu 0 może nie zostać uruchomiony na etapie 1 - byłoby to równoważne z uruchomieniem dowolnego programu regularnego (nie tylko metaprogramu) w czasie kompilacji.
Zgodnie z konwencją (i dla uproszczenia implementacji) kod w bieżącym module jest zawsze etapem 0, a kod importowany ze wszystkich innych modułów jest etapem 1. Z tego powodu można składać tylko wyrażenia z innych modułów.
Zauważ, że program etapu 1 jest wyrażeniem stopnia 0 typu Q Exp
, Q Type
itp .; ale odwrotność nie jest prawdą - nie każda wartość (program etapu 0) typu Q Exp
jest programem etapu 1,
Ponadto, ponieważ spawy mogą być zagnieżdżane, identyfikatory mogą mieć etapy większe niż 1. Ograniczenie etapu może być następnie uogólnione - program etapu n nie może być wykonany w żadnym etapie m> n . Na przykład w niektórych komunikatach o błędach można zobaczyć odniesienia do takich etapów większych niż 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 |]
Korzystanie z szablonu Haskell powoduje błędy poza zakresem z niepowiązanych identyfikatorów?
Zwykle wszystkie deklaracje w jednym module Haskell można traktować jako wzajemnie rekurencyjne. Innymi słowy, każda deklaracja najwyższego poziomu wchodzi w zakres każdego innego w jednym module. Gdy szablon Haskell jest włączony, zmieniają się zasady określania zakresu - moduł jest zamiast tego dzielony na grupy kodu oddzielone splajnami TH, a każda grupa jest wzajemnie rekurencyjna, a każda grupa jest w zakresie wszystkich dalszych grup.
Typ Q
Konstruktor typu Q :: * -> *
zdefiniowany w Language.Haskell.TH.Syntax
jest abstrakcyjnym typem reprezentującym obliczenia, które mają dostęp do środowiska czasu kompilacji modułu, w którym wykonywane jest obliczenie. Typ Q
obsługuje również podstawianie zmiennych, nazywane przechwytywaniem nazw przez TH (i omówione tutaj .) Wszystkie spawy mają typ QX
dla niektórych X
Środowisko czasu kompilacji obejmuje:
- identyfikatory w zakresie i informacje o tych identyfikatorach,
- rodzaje funkcji
- typy i typy danych źródłowych konstruktorów
- pełna specyfikacja deklaracji typu (klasy, rodziny typów)
- lokalizacja w kodzie źródłowym (linia, kolumna, moduł, pakiet), w której występuje połączenie
- poprawności funkcji (GHC 7.10)
- włączone rozszerzenia GHC (GHC 8.0)
Typ Q
ma również możliwość generowania świeżych nazw, z funkcją newName :: String -> Q Name
. Zauważ, że nazwa nie jest nigdzie związana, więc użytkownik musi ją sami powiązać, więc upewnienie się, że wynikowe użycie nazwy jest odpowiednio zakrojone, jest obowiązkiem użytkownika.
Q
ma instancje dla Functor,Monad,Applicative
i jest to główny interfejs do manipulowania wartościami Q
, wraz z kombinatorami dostępnymi w Language.Haskell.TH.Lib
, które definiują funkcję pomocniczą dla każdego konstruktora TH ast postaci:
LitE :: Lit -> Exp
litE :: Lit -> ExpQ
AppE :: Exp -> Exp -> Exp
appE :: ExpQ -> ExpQ -> ExpQ
Należy zauważyć, że ExpQ
, TypeQ
, DecsQ
i PatQ
są synonimami typów AST, które są zwykle przechowywane w typie Q
Biblioteka TH udostępnia funkcję runQ :: Quasi m => Q a -> ma
, i istnieje instancja Quasi IO
, więc wydaje się, że typ Q
jest tylko fantazyjnym IO
. Jednak użycie runQ :: Q a -> IO a
powoduje akcję IO
która nie ma dostępu do żadnego środowiska kompilacji - jest dostępna tylko w rzeczywistym typie Q
Takie działania IO
zakończą się niepowodzeniem w czasie wykonywania, jeśli spróbujesz uzyskać dostęp do wspomnianego środowiska.
Curry n-arowe
Znajomy
curry :: ((a,b) -> c) -> a -> b -> c
curry = \f a b -> f (a,b)
funkcję można uogólnić na krotki dowolnego rodzaju, na przykład:
curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d
curry4 :: ((a, b, c, d) -> e) -> a -> b -> c -> d -> e
Jednak ręczne pisanie takich funkcji dla krotek od 2 do (np.) 20 byłoby żmudne (i ignorowanie faktu, że obecność 20 krotek w twoim programie prawie na pewno sygnalizuje problemy projektowe, które należy naprawić za pomocą rekordów).
Możemy użyć szablonu Haskell do stworzenia takich funkcji curryN
dla dowolnego n
:
{-# LANGUAGE TemplateHaskell #-}
import Control.Monad (replicateM)
import Language.Haskell.TH (ExpQ, newName, Exp(..), Pat(..))
import Numeric.Natural (Natural)
curryN :: Natural -> Q Exp
Funkcja curryN
przyjmuje liczbę naturalną i tworzy funkcję curry tego arsenału jako Haskell AST.
curryN n = do
f <- newName "f"
xs <- replicateM (fromIntegral n) (newName "x")
Najpierw tworzymy zmienne typu świeżego dla każdego argumentu funkcji - jeden dla funkcji wejściowej i jeden dla każdego argumentu dla tej funkcji.
let args = map VarP (f:xs)
Wyrażenie args
reprezentuje wzór f x1 x2 .. xn
. Zauważ, że wzorzec jest osobną jednostką składniową - moglibyśmy wziąć ten sam wzorzec i umieścić go w lambda, powiązaniu funkcji, a nawet w LHS wiązania let (co byłoby błędem).
ntup = TupE (map VarE xs)
Funkcja musi zbudować krotkę argumentu z sekwencji argumentów, co właśnie zrobiliśmy tutaj. Zwróć uwagę na różnicę między zmiennymi VarP
( VarP
) a zmiennymi wyrażeniowymi ( VarE
).
return $ LamE args (AppE (VarE f) ntup)
Wreszcie, wartość, którą produkujemy, to AST \f x1 x2 .. xn -> f (x1, x2, .. , xn)
.
Moglibyśmy również napisać tę funkcję za pomocą cytatów i „podniesionych” konstruktorów:
...
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)) |]
Pamiętaj, że cytaty muszą być poprawne pod względem składniowym, więc [| \ $(map varP (f:xs)) -> .. |]
jest niepoprawna, ponieważ w zwykłym Haskell nie ma możliwości zadeklarowania „listy” wzorów - powyższe interpretowane jest jako \ var -> ..
i splicowane wyrażenie ma mieć typ PatQ
, tj. pojedynczy wzorzec, a nie listę wzorców.
Wreszcie możemy załadować tę funkcję TH w 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
Ten przykład jest dostosowany przede wszystkim stąd .
Składnia szablonu Haskell i quasiquotes
Szablon Haskell jest włączony przez rozszerzenie GHC -XTemplateHaskell
. To rozszerzenie włącza wszystkie funkcje syntaktyczne szczegółowo opisane w tej sekcji. Pełne informacje na temat szablonu Haskell podano w instrukcji obsługi .
Splice
Splot to nowy obiekt składniowy włączony przez szablon Haskell, zapisany jako
$(...)
, gdzie(...)
jest pewnym wyrażeniem.Pomiędzy
$
a pierwszym znakiem wyrażenia nie może być spacji; a Szablon Haskell zastępuje parsowanie operatora$
- np.f$g
jest zwykle analizowane jako($) fg
natomiast przy włączonym szablonie Haskell jest analizowane jako połączenie.Gdy złącze pojawi się na najwyższym poziomie,
$
może zostać pominięte. W takim przypadku łączone wyrażenie jest całą linią.Splot reprezentuje kod, który jest uruchamiany w czasie kompilacji w celu wytworzenia Haskell AST, i że AST jest kompilowany jako kod Haskell i wstawiany do programu
Złączenia mogą pojawiać się zamiast: wyrażeń, wzorów, typów i deklaracji najwyższego poziomu. Typ składanego wyrażenia, odpowiednio, w każdym przypadku to
Q Exp
,Q Pat
,Q Type
,Q [Decl]
. Pamiętaj, że splice deklaracji mogą pojawiać się tylko na najwyższym poziomie, podczas gdy inne mogą znajdować się odpowiednio w innych wyrażeniach, wzorach lub typach.
Cytaty wyrażeń (uwaga: nie quasi-quuotacja)
Cytat wyrażenia jest nowym bytem składniowym zapisanym jako jeden z:
-
[e|..|]
lub[|..|]
-..
jest wyrażeniem, a cytat ma typQ Exp
; -
[p|..|]
-..
jest wzorem, a cytat ma typQ Pat
; -
[t|..|]
-..
jest typem, a cytat ma typQ Type
; -
[d|..|]
-..
to lista deklaracji, a oferta ma typQ [Dec]
.
-
Cytat z wyrażenia wymaga kompilacji programu czasowego i tworzy wartość AST reprezentowaną przez ten program.
Zastosowanie wartości w cudzysłowie (np.
\x -> [| x |]
) bez splicingu odpowiada cukierowi syntaktycznemu dla\x -> [| $(lift x) |]
, gdzielift :: Lift t => t -> Q Exp
pochodzi z klasy
class Lift t where lift :: t -> Q Exp default lift :: Data t => t -> Q Exp
Wpisane splice i cytaty
Typowane spawy są podobne do wcześniej wymienionych (nietypowanych) splotów i są zapisywane jako
$$(..)
gdzie(..)
jest wyrażeniem.Jeśli
e
ma typQ (TExp a)
to$$e
ma typa
.Cytowane typy mają postać
[||..||]
gdzie..
jest wyrażeniem typua
; wynikowy cytat ma typQ (TExp a)
.Wpisane wyrażenie można przekonwertować na te bez typu:
unType :: TExp a -> Exp
.
QuasiQuotes
QuasiQuotes uogólniają cytaty wyrażeń - wcześniej parser używany przez cytat wyrażenia był jednym z ustalonego zestawu (
e,p,t,d
), ale QuasiQuotes pozwala na zdefiniowanie niestandardowego analizatora składni i użycie go do wygenerowania kodu w czasie kompilacji. Quasi-cytaty mogą pojawiać się w tych samych kontekstach, co zwykłe cytaty.Quasi-cytat zapisywany jest jako
[iden|...|]
, gdzieiden
jest identyfikatorem typuLanguage.Haskell.TH.Quote.QuasiQuoter
.QuasiQuoter
składa się po prostu z czterech parserów, po jednym dla każdego z różnych kontekstów, w których mogą pojawiać się cytaty:
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp, quotePat :: String -> Q Pat, quoteType :: String -> Q Type, quoteDec :: String -> Q [Dec] }
Nazwy
Identyfikatory Haskell są reprezentowane przez typ
Language.Haskell.TH.Syntax.Name
. Nazwy tworzą liście abstrakcyjnych drzew składniowych reprezentujących programy Haskell w szablonie Haskell.Identyfikator, który jest obecnie objęty zakresem, może zostać przekształcony w nazwę z:
'e
'T
lub'T
. W pierwszym przypadkue
jest interpretowane w zakresie wyrażenia, podczas gdy w drugim przypadkuT
jest w zakresie typu (przypominając, że konstruktory typów i wartości mogą dzielić nazwę bez dwuznaczności w Haskell).