GHC 공식 문서 - 6.13. Template Haskell
Basic Tutorial of Template Haskell
24 Days of GHC Extensions: Template Haskell
프로그래밍 흑마법: TemplateHaskell - 김은민님
C언어의 전처리기쯤으로 생각하고, 특정 $(...)
을 만나면 코드로 대체하겠거니 하고, 자세한 공부 없이 써오고 있었습니다. 미루다 미루다 드디어 보기 시작했는데, 전처리기보다는 더 강력한 작업을 할 수 있는 걸로 보입니다. 여기서는 Template Haskell을 쓰는 정도의 이해를 하는 게 목표입니다. 자세한 동작 방식을 공부하려면 역시나 논문에 논문으로 들어 갑니다.
“Metaprogramming은 C보다는 LISP 쪽에서 접근하는 게 더 낫습니다. LISP의 quasiquotation을 좀 더 복잡한, 컴파일 하는 언어에 맞춰 적용한 것으로 TemplateHaskell 등이 있습니다. - @Ailrun”
하스켈 코드를 생성하기 위한 언어(문법), 즉 메타 언어를 별도의 다른 언어나, 문법을 가져온 게 아니라, 일반 하스켈을 그대로 썼기 때문에 더 강력다고 합니다.
Template Meta-programming for haskell - Tim Sheard, SPJ
Template Haskell은 하스켈 코드를 생성하는 하스켈 코드 작성이 목표입니다. 라이브러리가 Template Haskell을 쓴다고 하면, 제일 먼저 $(...)
구문을 만납니다. 여기다 “하스켈 코드”를 생성하는 하스켈 코드를 넣어주면, Template Haskell 컴파일을 거쳐 생성된 “하스켈 코드”로 대체됩니다. 여기서 “하스켈 코드”를 생성하는 하스켈 코드는
InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2)))
이런 모양을 넣어주게 됩니다. 보기 까다롭긴 한데, 일반적으로 작성하는 하스켈 문법과 다를 바 없습니다. 이 코드의 의미는 하스켈 문법의 AST(Abstract Syntax Tree)입니다.
※ 사람이 작성한 보통의 하스켈 코드는 GHC가 컴파일 타임에 AST로 뽑아내는데,이 때 나오는 AST와는 다른 것입니다. TH가 하스켈 코드를 생성하기 위한 룰에만 적합한 AST를 따로 만들어서 씁니다.
예를 들어, 함수를 호출하는 코드 f x
의 AST는 AppE (VarE f) (VarE x)
입니다. (당장 VarE
가 뭔지, AppE
가 뭔지 알 필요는 없습니다.) 이런 AST를 만들어서 $(...)
안에 두면, 나중에 Template Haskell 컴파일을 거치면 f x
를 써 준 것과 같은 상태가 됩니다. 이하 Template Haskell은 TH
로 표기하겠습니다.
위와 같은 InfixE ...
같은 구문 트리Syntax Tree를 직접 작성하려면 여간 까다로운게 아닙니다. 그래서 TH
는 Quotations를 제공합니다. Quotations는 AST를 생성하는 편의sugar 문법으로 볼 수 있습니다.
TH의 “템플릿”으로 볼 수 있습니다. the “template” of Template Haskell. Quasi-quotation이라 부릅니다. 따옴표로 인용Quote하듯 바와 각괄호가 있는 [| ... |]
을 써서 인용Quote합니다. ※ 이 괄호를 옥스포드 괄호라 부릅니다. 찾아 보니 ⟦ ⟧
이렇게 생긴 괄호라 하는데, 똑같이 보이진 않지만, 어쨌든 그렇게 부른다고 합니다.
[| 1 + 2 |]
이렇게 쓰면 위와 동일한 AST, 즉 InfixE (Just (...
를 생성해 줍니다.
그 다음, 이 AST를 $(...)
에 넣으면, 하스켈 코드를 생성하는 걸 볼 수 있습니다.
ghci> [| 1 + 2 |]
InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2)))
ghci> $(pure it) 3
$(pure it)
는 1 + 2
코드를 생성했습니다.
※ 여기서 it
을 안쓰고 직접 써주면 예상과 달리 오류가 나옵니다.1
※ splice 2 는 $x
형태로 씁니다. x
에는 표현식이 들어 갑니다. 이미 중위 연산자로 쓰이는 $
가 있으므로, 이와 구별하기 위해 $
와 x
사이에 공백이 있으면 안됩니다. M.x
가 중위 연산자 .
와 구별하기 위해 붙여쓰는 것과 마찬가지입니다. $
와 .
을 중위 연산자로 쓰려면 앞뒤로 공백을 넣어 주면 됩니다.
아래는 Bulat’s 튜토리얼에서 발췌했습니다. (원 사이트에 있는 소스가 현재 제가 쓰는 GHC 9.4.2와는 맞지 않아 VarP id
를 [VarP id]
로, VarE id
를 Just (VarE id)
로 수정했습니다.)
module TupleReplicate where
import Language.Haskell.TH
tupleReplicate :: Int -> Q Exp
= do id <- newName "x"
tupleReplicate n return $ LamE [VarP id]
TupE $ replicate n $ Just (VarE id)) (
이 걸 splice에 집어 넣으면, 아래 코드를 생성합니다.
-> (x, x, x)) (\x
이 코드에 "x"
인자를 주어 실행하면,
ghci> :set -XTemplateHaskell
ghci> $(tupleReplicate 3) "x" ("x","x","x")
= fail ($(printf "Error in file %s line %d") file line) yell file line
TH
가 아래 코드를 생성합니다.
= fail ((\x1 x2 -> "Error in file "++x1++" line "++show x2) file line) yell file line
data T = A Int String | B Integer | C
$(deriveShow ''T)
TH
가 아래 코드를 생성합니다.
data T = A Int String | B Integer | C
instance Show T
show (A x1 x2) = "A "++show x1++" "++show x2
show (B x1) = "B "++show x1
show C = "C"
(작성 중)
Identifier는 mkName
이나 dyn
으로 정의해야 합니다.
ghci> $(pure $ InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2))))
<interactive>:56:45: error:
• Couldn't match expected type ‘Exp’
with actual type ‘(Name -> Exp) -> Name -> Exp’ • In the second argument of ‘InfixE’, namely ‘(VarE +)’
“Variable identifier가 show로 그냥 나온 거예요” - @Ailrun
위와 같이 GHC.Num.+
가 코드에 쓰이면 바로 함수로 인식합니다. splice에서 쓰려면 mkName
으로 captureable name
을 만들어 써야 합니다.
ghci> :{
ghci| $(pure $ InfixE (Just (LitE (IntegerL 1)))
ghci| (VarE $ mkName "GHC.Num.+")
ghci| (Just (LitE (IntegerL 2))))
ghci| :} 3
+
라는 식별자identifier는 AST에서 쓰려면 mkName으로 정의되어야 합니다.↩︎
Splice: 메타 프로그래밍에서 쓰는 용어로, 나중에 대체될 대상들을 의미합니다. TH
에서는 $(...)
를 의미합니다. 영단어 뜻은 필름과 필름을 이어 붙인 부분을 의미합니다.
add a splice plate
는 무언가에 덧대어 붙이는 걸 말합니다. 우리말로 번역하면 중첩부, 연결부, 이음부 쯤입니다.↩︎