Template Haskell(작성 중)

Posted on January 13, 2023

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 문법으로 볼 수 있습니다.

Quotations

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가 중위 연산자 .와 구별하기 위해 붙여쓰는 것과 마찬가지입니다. $.을 중위 연산자로 쓰려면 앞뒤로 공백을 넣어 주면 됩니다.

예시

TupleReplicate

아래는 Bulat’s 튜토리얼에서 발췌했습니다. (원 사이트에 있는 소스가 현재 제가 쓰는 GHC 9.4.2와는 맞지 않아 VarP id[VarP id]로, VarE idJust (VarE id)로 수정했습니다.)

module TupleReplicate where
import Language.Haskell.TH

tupleReplicate :: Int -> Q Exp
tupleReplicate n = do id <- newName "x"
                      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")

printf

yell file line = fail ($(printf "Error in file %s line %d") file line)

TH가 아래 코드를 생성합니다.

yell file line = fail ((\x1 x2 -> "Error in file "++x1++" line "++show x2) file line)

deriveShow

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"

(작성 중)


  1. 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으로 정의되어야 합니다.↩︎

  2. Splice: 메타 프로그래밍에서 쓰는 용어로, 나중에 대체될 대상들을 의미합니다. TH에서는 $(...)를 의미합니다. 영단어 뜻은 필름과 필름을 이어 붙인 부분을 의미합니다.

    add a splice plate

    는 무언가에 덧대어 붙이는 걸 말합니다. 우리말로 번역하면 중첩부, 연결부, 이음부 쯤입니다.↩︎

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com