A Haskell programozási nyelv

Template Haskell

Makrózás, metaprogramozás

Makrónak nevezzük az olyan programokat, amelyek a fordítási folyamat során futnak le, és eredményük a makróhívás helyére épül be. Ezáltal lehetőségünk van a forráskód magasabb szintű szerkezeteinek automatizált generálására. Ilyenkor a makrók használatával a program áttekinthetőbbé válik, valamint a megírt makrók más programokban is felhasználhatók. A metaprogramozás azt a programozási módszert jelenti, amikor a fordítási időben végrehajtott makrók és a futásidőben végrehajtott programok "összekeverednek", és a kétszintű hierarchia helyét bonyolult kölcsönhatások rendszere veszi át.

A Template Haskell egy Haskell98-kiegészítés, amely lehetőséget ad a típusbiztos fordítási idejű meta-programozásra. Template Haskell esetén a makrókat magukat is Haskell nyelven írjuk.

Haskell nyelvű programokban akkor lehet makrókra szükségünk, amikor függvényekel nem írható le az az általánosítás, aminek a szintjén a megoldásunkat meg akarjuk fogalmazni, például azért, mert nem lenne típusozható.

Példa: Vetítőfüggvények

A továbbiakban egy konkrét példán keresztül mutatjuk be a Template Haskell működését. A példánk a standard Prelude-ben megtalálható fst illetve snd függvények általánosítása lesz. A két függvény típusa, illetve definíciója az alábbi:

fst :: (a, b) -> a fst (x, y) = x
snd :: (a, b) -> b snd (x,y) = y

Ezek tehát a rendezett párok vetítő-függvényei: az fst egy rendezett pár első elemét, míg az snd a második elemét adja vissza. Hasonlóképpen megírhatnánk a vetítést rendezett hármasokra is:

fst3 (x, y, z) = x snd3 (x, y, z) = y trd3 (x, y, z) = z

De mi a helyzet, ha általában rendezett n-esek k-adik tagját elérő függvényt akarunk készíteni? Nyilván nem írhatunk olyan függvényt, amelyiknek az n és a k futásidejű paramétere, hiszen például a függvény visszatérési típusa függ k-tól, ahogyan az fst illetve snd típusának összevetéséből is látszik.

Természetesen adott n-re illetve k-ra elkészíthetjük a vetítőfüggvényeket, ahogyan azt fent n=2-re illetve n=3-ra meg is tettük, vagy például n=4, k=2-re:

proj_4_2 :: (a, b, c, d) -> b proj_4_2 (x, y, z, w) = y

A Template Haskell arra ad megoldást, hogy tudjunk írni olyan programot, amely fordítási időben előállítja a fenti alakú függvényeket. Ez a függvény beépül a fordítóprogramba, és tetszőleges n-re illetve k-ra generáltathatunk vele vetítőfüggvényeket.

Haskell kód generálása

A Template Haskell tehát olyan, Haskell nyelvű programok írására szolgál, amelyeknek eredménye is Haskell nyelvű program. Ehhez természetesen szükség van egy interfészre, ami a makrók és a fordítóprogram között van.

Ez a gyakorlatban azt jelenti, hogy a Template Haskell részét képezik a Haskell kifejezések, minták és definíciók reprezentálására alkalmas algebrai adattípusok, és a makrók ebben a reprezentációban állítják elő a kimenetüket. Ez a reprezentáció gyakorlatilag megfelel a Haskell kifejezések szintaxisfájának.

Például az előbbiekben bemutatott fst3 (avagy proj_3_1) függvény definíciója (az egyszerűség kedvéért minták helyett lambda-kifejezést használva):

proj_3_1 = \ (x, y, z) -> x

A jobboldali lambda-kifejezés TH reprezentációja:

LamE [TupP [ VarP "x", VarP "y", VarP "z"]] VarE "x"

A készítendő proj makrónk feladata tehát, hogy a fenti szintaxisfát adott n-re illetve k-ra elkészítse. Ez történhet például a következő programmal:

proj n k = let vars = map mkVar [1..n] in LamE [TupP (map VarP vars)] (VarE (vars!!(k-1))) where mkVar i = mkName ("x" ++ show i)

Kipróbálhatjuk, hogy n=3-ra és k=1-re (átnevezésektől eltekintve) visszakapjuk a fenti kifejezés szintaxisfáját:

proj 3 1 == LamE [TupP [VarP "x1", VarP "x2", VarP "x3"]] (VarE "x1")

Egyedi nevek biztosítása

A makróknak szüksége lehet egyedi azonosító-nevek generálására, nehogy a makróalkalmazás helyén lévő nevek összegabalyodjanak a makróból kieső nevekkel. A Template Haskellben ezért a makróknak az egyedi nevek generálását végző Q monádban kell a számításaikat elvégezni. Ebben a monádban a newName nevű kiszámítással hozhatunk létre új, valamilyen adott prefixű neveket.

A fenti makró végleges alakja tehát az alábbi:

module Proj where import Language.Haskell.TH import Control.Monad proj :: Int -> Int -> Q Exp proj n k = do vars <- replicateM n (newName "x") return $ LamE [TupP (map VarP vars)] (VarE (vars!!(k-1)))

Mi a teendő, ha ki akarjuk próbálni a makrónkat? Kézenfekvő ötlet, hogy GHCi-ből megnézzük, mit is állít elő. Ehhez azonban szükségünk van a Q monád futtatására. A runQ függvény segítségével a Q-beli kiszámításokat "felemelhetjük" az IO monádba:

*Proj> runQ (proj 3 1) LamE [TupP [VarP x_18,VarP x_19,VarP x_20]] (VarE x_18) *Proj> runQ (proj 3 1) LamE [TupP [VarP x_21,VarP x_22,VarP x_23]] (VarE x_21)

Haskell kód "idézőjelezése"

A szintaxisfa előállításának egy kényelmes módját jelenti a Template Haskell azon lehetősége, hogy a [|.|] jelek közé zárt kifejezések értéke a benne lévő kifejezés szintaxisfája, a változónevek konzekvens egyediesítésével. Például az alábbi két program ekvivalens:

[| \(x, y) -> (x, x)|] do x <- newName "x" y <- newName "y" return $ LamE [TupP [VarP x, VarP y]] (TupE [VarE x, VarE x])

Makrók felhasználása

A fentieknek megfelelően definiált makrókat a $() szintaxis segítségével használhatjuk fel a programunkban. A $() zárójelei közötti kifejezést fordítási időben, egy alkalmas Q monádban számítja ki a fordítóprogram, és az eredményével helyettesíti a forrásban.

Fontos megkötés, hogy egy modulban nem használhatunk ugyanabban a modulban definiált makrót. Kivételt képez ez alól a [|.|] jelekkel "idézőjelbe tett" kód belseje: itt tetszőleges belső hivatkozás is állhat.

Például az alábbi program a fenti módon definiált proj makrót használja:

{-# LANGUAGE TemplateHaskell#-} module Hello where import Proj p = $(proj 4 2) main = print $ p (1, 2, 3, 4)