Template Haskell은 컴파일 타임 메타프로그래밍을 제공하는 GHC 확장으로, 구문 트리(AST)를 Haskell 코드로 생성·변환·스플라이스할 수 있다.
**Template Haskell**은 Haskell에 컴파일 타임 메타프로그래밍 기능을 추가하는 GHC 확장이다. 원래 설계는 여기에서 찾을 수 있다. Template Haskell은 GHC 6 버전부터 포함되어 있다.
GHC Users Guide에는 Template Haskell 섹션이 있다.
이 페이지는 TH 관련 내용을 좀 더 중심적이고 정리된 형태로 모아두는 저장소가 되기를 바란다.
Template Haskell은 Haskell 98에 대한 확장으로, Haskell을 조작하는 언어이자 조작 대상 언어로 사용하면서 타입 안전한 컴파일 타임 메타프로그래밍을 할 수 있게 해준다.
직관적으로 Template Haskell은 구체 구문(concrete syntax), 즉 일반적인 Haskell 코드를 작성할 때 여러분이 타이핑하는 형태,와 추상 구문 트리(abstract syntax tree) 사이를 오갈 수 있게 해주는 새로운 언어 기능을 제공한다. 이 추상 구문 트리는 Haskell 데이터 타입으로 표현되며, 컴파일 시점에 Haskell 코드로 조작될 수 있다. 이를 통해 어떤 코드를 재화(reify: 구체 구문을 추상 구문 트리로 변환)하여 변환한 뒤 다시 스플라이스(splice: 다시 되돌려 삽입)할 수도 있고, 또는 컴파일러가 모듈을 컴파일하는 동안 완전히 새로운 코드를 만들어 스플라이스할 수도 있다.
Template Haskell 관련 이메일은 GHC users 메일링 리스트를 사용하라. TH를 사용하기 시작한다면 가입할 만하다.
Template Haskell은 현재로서는 다소 비공식적으로만 문서화되어 있다. 주요 자료는 다음과 같다:
한 독자는 “이 문서들은 정말 훌륭하다! TH를 이해하는 데 정확히 내가 필요하던 것.”이라고 말했다. (참고: 원본이 사라져서 Wayback machine에 있는 문서들이다. Google docs의 공개 문서라 로그인 없이 볼 수 있어야 한다. 하지만 보기 위해 로그인하라는 메시지가 뜬다면, 알려진 Google 버그에 걸린 것이다. Google로 접속해(아마 쿠키가 생긴 뒤에) 다시 시도하면 해결된다고 한다.)
Dominik의 예제 중심의 실용적인 Template Haskell 입문.
10분 안에 기본을 이해하기 위한 아주 짧은 튜토리얼.
GHC Template Haskell 문서
Template Haskell 관련 논문
(2011) Yesod 맥락에서의 Greg Weber의 Template Haskell 및 준인용(quasi-quoting)에 대한 블로그 글.
(2012) 보간 텍스트 QuasiQuoter를 만들기 위한 Mike Ledger의 TemplateHaskell 및 QuasiQuotation 튜토리얼. 또 다른 2014년의 준인용에 대한 훌륭한 글도 있다.
Fraskell 문서 및 Template Haskell로 이를 크게 가속하는 방법에 대한 설명.
Wikipedia 항목 http://en.wikipedia.org/wiki/Template_Haskell도 자유롭게 업데이트해 달라.
Template Haskell로 무엇을 하고 있거나/할 계획이 있거나/해본 적이 있는가?
ForSyDe 방법론은 현재 Template Haskell을 광범위하게 사용하는 Haskell 기반 DSL로 구현되어 있다.
Mac OS X의 Objective-C 런타임 시스템에 대한 원시적(비타입) 바인딩을 작성했다. TH만 필요하고 “stub 파일”을 만들지 않으며 별도의 유틸리티도 필요 없다. 초기 스냅샷: http://www.kfunigraz.ac.at/imawww/thaller/wolfgang/HOC020103.tar.bz2 — WolfgangThaller
Template Greencard를 작성 중이다 — TH를 사용하여 GreenCard를 재구현하는 것이다. 여러 부분이 정말 잘 맞아떨어졌지만, 일부는 그다지 좋지 않았다. 시간이 나면 TH 쪽에 변경을 설득해 이 문제를 해결해 보겠다. — AlastairReid
Hacanon을 작성 중이다 — C++ 바인딩 자동 생성을 위한 프레임워크. “C++용 자동화된 Template Greencard”를 읽어보라 (-: Darcs repo: http://www.ScannedInAvian.org/repos/hacanon — 예제를 컴파일하려면 gccxml(http://www.gccxml.org/)이 필요하다. — 12월 27일 Lemmih.
다른 FFI 도구 개발자들을 따라가며, 특히 C로 정의된 구조체의 peek/poke 메서드를 자동 생성하는 쪽에서 Template HSFFIG의 미래가 있다고 본다. X11처럼 메시지 레이아웃이 C의 구조체/유니언 선언으로 제공되는 특정 네트워크 프로토콜 구현에 유용할 수 있다. — 2005년 12월 16일 DimitryGolubovsky
Template Haskell을 사용해 파싱되고 타입체크된 코드를 Ajax 기반의 Haskell 등식 추론 도구인 Haskell Equational Reasoning Assistant에 넣는 메커니즘으로 사용하고 있으며, 코드 조각 사이의 등식 관계 명세를 단순화하고 있다. 도구 사용 모습을 담은 quicktime 영화가 http://www.gill-warbington.com/home/andy/share/hera1.html에 있었다. — AndyGill
기존 컴파일러 기술의 상당 부분을 재사용하여 프로그래밍 신뢰성과 생산성을 높이기 위한 함수형 메타프로그래밍 기법을 연구 중이다. Template Haskell은 컴파일 시점에 이용 가능한 구조의 크기 정보를 컴파일러가 확인할 수 있게 해주기 때문에 특히 흥미롭다. 이 접근은 회로가 동작하기 전에 구조가 고정되는 하드웨어 설계에 특히 적합하다. 메타프로그래밍 웹 페이지: http://www.infosun.fmi.uni-passau.de/cl/metaprog/ — ChristophHerrmann(http://www.cs.st-and.ac.uk/~ch)
Template Haskell을 타입 안전한 데이터베이스 접근에 사용하고 있다. 처음에는 haskell-cafe에 제안했다. 컴파일 타임에 DB에 연결해 SQL 파싱과 타입 추론을 DB에게 맡긴다. 파싱/타입 추론 결과를 사용하여 런타임에 실행 가능한 타입 안전한 DB 쿼리를 구성한다. 프로젝트 페이지는 여기 — Mads Lindstrøm
헬퍼 함수, 디버깅 함수, 또는 TH.Syntax를 위한 모나딕 폴드 대수 같은 더 복잡한 코드.
GHC 버그 트래커에서 Template Haskell에 대한 열린 버그들을 살펴보라.
Ian Lynagh(Igloo)가 2003년 5월 논문 Template Haskell: A Report From The Field(여기에서 이용 가능)에서 언급한 것들을 섹션별로 정리하면 다음과 같다:
섹션 2 (curses)
foreign import 선언에)섹션 3 (클래스의 인스턴스 deriving)
reify 함수)Read, Show 인스턴스를 deriving하는 데 필요). (방법이 있다면 Derive는 그걸 모르는 듯하다.)instance 헤더에서)섹션 4 (printf)
섹션 5 (fraskell)
(+),이 있어도 안전하게 단순화하기 위해)섹션 6 (pan)
(구현된 것들은 여기 남겨두되, 취소선으로 표시해 달라.)
그 외 유용할 법한 기능, 그리고 보고 싶은 TH 프로젝트.
reify가 그 클래스의 인스턴스 목록을 반환하도록 하기(http://www.haskell.org/pipermail/template-haskell/2005-December/000503.html). (또한 GHC 티켓 #1577 참고.)템플릿의 한가운데에 무언가를 스플라이스하려다 도저히 안 될 때, 그 때문에 짜증내기보다는, 템플릿을 이용해 그것이 장황한 형태(longhand)로는 어떻게 보이는지 확인해 보는 게 어떨까?
먼저 내가 만든 모듈에서 발췌한 코드가 있다. 참고로 나는 SamB이다.
haskell{-# OPTIONS_GHC -fglasgow-exts -fth #-} module MMixMemory where import Data.Int import Data.Word class (Integral int, Integral word) => SignConversion int word | int -> word, word -> int where toSigned :: word -> int toSigned = fromIntegral toUnsigned :: int -> word toUnsigned = fromIntegral
예를 들어 SignConversion 클래스의 인스턴스 선언에 들어갈 타입들을 스플라이스하는 데 무엇을 해야 하는지 알고 싶다고 하자. 그래서 Int8/Word8부터 Int64/Word64까지 인스턴스를 선언하고 싶다. 그러면 좋은 옛날 GHCi를 실행해서 다음처럼 한다:
text$ ghci -fth -fglasgow-exts Prelude> :l MMixMemory *MMixMemory> :m +Language.Haskell.TH.Syntax *MMixMemory Language.Haskell.TH.Syntax> runQ [d| instance SignConversion Int Word where |] >>= print [InstanceD [] (AppT (AppT (ConT MMixMemory.SignConversion) (ConT GHC.Base.Int)) (ConT GHC.Word.Word)) []]
reify를 사용해 Name에 대한 정보를 얻으면, GHC는 자신이 아는 것을 말해준다. 하지만 가끔은 모르는 것도 있다. 특히:
import된 것들. 모듈 M에서 import한 함수, 타입 생성자, 클래스 등을 reify하면, GHC는 M을 컴파일할 때 알아낸 정보를 담아둔 인터페이스 파일 M.hi로 달려간다. 하지만 최적화 없이(즉 기본값인 -O0) 그리고 -XTemplateHaskell 없이 M을 컴파일했다면, GHC는 인터페이스 파일에 가능한 한 적은 정보를 넣으려 한다. (인터페이스 파일을 작게 유지하려는—어쩌면 잘못된—시도.) 특히 데이터 타입의 이름과 kind만 M.hi에 덤프하고 생성자(constructor)는 덤프하지 않을 수 있다.
이런 상황에서는 데이터 타입을 reify했는데도 데이터 생성자나 필드에 대한 정보가 전혀 없을 수 있다. 해결책: M을 다음 중 하나로 컴파일하라.
-O, 또는-fno-omit-interface-pragmas(-O에 의해 암시됨), 또는-XTemplateHaskell.함수 정의. Info 타입의 VarI 생성자는 함수 정의의 소스 코드를 돌려받을 수도 있다고 광고한다. 하지만 실제로 GHC는(7.4 기준) 이 필드에 항상 Nothing을 반환한다. 다소 불편하지만 정말로 그게 필요했던 사람은 없었다.
다음 프로그램은 실행하면 오류 메시지와 함께 실패한다:
haskellmain = do info <- runQ (reify (mkName "Bool")) -- 더 위생적인 방법: (reify ''Bool) putStrLn (pprint info)
이유: reify는 타입 환경(type environment)을 참조하는데, 런타임에는 그것을 사용할 수 없다. reify의 타입은 다음과 같다:
haskellreify :: Quasi m => Q a -> m a
IO 모나드는 Quasi의 빈약한 인스턴스(“poor-man's instance”)이다. 고유 이름을 만들고 오류 메시지를 수집할 수는 있지만, reify는 할 수 없다. 이 오류는 사실 정적으로 잡혀야 한다.
대신, 아래처럼 스플라이스를 직접 실행할 수 있다(예: ghci -XTemplateHaskell에서):
haskellGHCi> let tup = $(tupE $ take 4 $ cycle [ [| "hi" |] , [| 5 |] ]) GHCi> :type tup tup :: ([Char], Integer, [Char], Integer) GHCi> tup ("hi",5,"hi",5) GHCi> $(stringE . show =<< reify ''Int) "TyConI (DataD [] GHC.Types.Int [] [NormalC GHC.Types.I# [(NotStrict,ConT GHC.Prim.Int#)]] [])"
더 자세한 내용은 이 이메일 스레드를 참고하라.
임의 크기의 튜플에서 원소를 하나 선택하는 예제. 이 논문에서 가져왔다.
사용 예:
text> $(sel 2 3) ('a','b','c') 'b' > $(sel 3 4) ('a','b','c','d') 'c'
haskellsel :: Int -> Int -> ExpQ sel i n = [| \x -> $(caseE [| x |] [alt]) |] where alt :: MatchQ alt = match pat (normalB rhs) [] pat :: Pat pat = tupP (map varP as) rhs :: ExpQ rhs = varE(as !! (i -1)) -- !! 는 0 기반 as :: [Name] as = [ mkName $ "a" ++ show i | i <- [1..n] ]
대안:
haskellsel' i n = lamE [pat] rhs where pat = tupP (map varP as) rhs = varE (as !! (i - 1)) as = [ mkName $ "a" ++ show j | j <- [1..n] ]
haskelltmap i n = do f <- newName "f" as <- replicateM n (newName "a") lamE [varP f, tupP (map varP as)] $ tupE [ if i == i' then [| $(varE f) $a |] else a | (a,i') <- map varE as `zip` [1..] ]
tmap 호출 예:
text> $(tmap 3 4) (+ 1) (1,2,3,4) (1,2,4,4)
리스트에서 원소를 뽑아 튜플을 만드는 예제. www.xoltar.org에서 가져왔다.
사용 예:
text> $(tuple 3) [1,2,3,4,5] (1,2,3) > $(tuple 2) [1,2] (1,2)
haskelltuple :: Int -> ExpQ tuple n = [|\list -> $(tupE (exprs [|list|])) |] where exprs list = [infixE (Just (list)) (varE "!!") (Just (litE $ integerL (toInteger num))) | num <- [0..(n - 1)]]
더 정보성 있는 오류를 제공하는 대안(패턴 매칭 실패가 정확한 위치를 준다):
haskelltuple :: Int -> ExpQ tuple n = do ns <- replicateM n (newName "x") lamE [foldr (\x y -> conP '(:) [varP x,y]) wildP ns] (tupE $ map varE ns)
길이를 주면 (a,(b,(c,()))) 같은 중첩 튜플을 (a,b,c)로 바꾼다.
haskellunNest n = do vs <- replicateM n (newName "x") lamE [foldr (\a b -> tupP [varP a , b]) (conP '() []) vs] (tupE (map varE vs))
이 접근은 Template Haskell을 사용해 타입체크를 지연시켜 fromDynamic의 반복 호출을 추상화하는 예다:
haskelldata T = T Int String Double toT :: [Dynamic] -> Maybe T toT [a,b,c] = do a' <- fromDynamic a b' <- fromDynamic b c' <- fromDynamic c return (T a' b' c') toT _ = Nothing
다음과 비슷한 명령으로 빌드하라:
textghc --make Main.hs -o main
Main.hs:
haskell{-# LANGUAGE TemplateHaskell #-} -- 템플릿 "printf"를 임포트 import PrintF (printf) -- 스플라이스 연산자 $는 -- "printf"가 컴파일 타임에 생성한 Haskell 소스 코드를 받아 -- "putStrLn" 인자 안에 스플라이스한다. main = do putStrLn $ $(printf "Hello %s %%x%% %d %%x%%") "World" 12
PrintF.hs:
haskell{-# LANGUAGE TemplateHaskell #-} module PrintF where -- NB: printf는 사용하려는 모듈과는 별도의 모듈에 있어야 한다. -- Template Haskell 구문을 임포트 import Language.Haskell.TH -- 가능한 문자열 토큰: %d %s 그리고 리터럴 문자열 data Format = D | S | L String deriving Show -- 빈약한 토크나이저 tokenize :: String -> [Format] tokenize [] = [] tokenize ('%':c:rest) | c == 'd' = D : tokenize rest | c == 's' = S : tokenize rest tokenize (s:str) = L (s:p) : tokenize rest -- 이상한 '%'에서 멈추지 않게 where (p,rest) = span (/= '%') str -- 함수의 인자 리스트 생성 args :: [Format] -> [PatQ] args fmt = concatMap (\(f,n) -> case f of L _ -> [] _ -> [varP n]) $ zip fmt names where names = [ mkName $ 'x' : show i | i <- [0..] ] -- 함수 본문 생성 body :: [Format] -> ExpQ body fmt = foldr (\ e e' -> infixApp e [| (++) |] e') (last exps) (init exps) where exps = [ case f of L s -> stringE s D -> appE [| show |] (varE n) S -> varE n | (f,n) <- zip fmt names ] names = [ mkName $ 'x' : show i | i <- [0..] ] -- 인자 리스트와 본문을 람다로 접착 -- 이것이 "printf" 호출 지점에서 haskell 코드로 스플라이스된다 printf :: String -> Q Exp printf format = lamE (args fmt) (body fmt) where fmt = tokenize format
GetOpt 등에서 온 옵션 집합을 다루는 흔한 관용구는 모든 플래그를 포함하는 데이터 타입을 정의하고 그 데이터 타입의 리스트를 쓰는 것이다:
haskelldata Options = B1 | B2 | V Integer options = [B1, V 3]
불리언 플래그가 설정되었는지 테스트하는 건(그냥 elem을 쓰면 되므로) 간단하지만, 인자를 가진 옵션이 설정되었는지 확인하는 것은 더 어렵다. 게다가 그런 옵션에서 값을 얻어오는 헬퍼 함수를 쓰는 것도 번거로운데, 매번 명시적으로 V를 “벗겨(un-V)”야 하기 때문이다. 여기서 Template Haskell을 (남용하여) 이를 조금 줄일 수 있다. 아래 예제는 Options의 생성자와 무관하게 재사용 가능한 OptionsTH 모듈을 제공한다. 먼저 우리가 어떻게 프로그래밍하고 싶은지부터 보자. 단, 결과 리스트는 foldl 등으로 추가 처리가 필요할 수 있다.
Options.hs:
haskellmodule Main where import OptionsTH import Language.Haskell.TH.Syntax data Options = B1 | B2 | V Int | S String deriving (Eq, Read, Show) options = [B1, V 3] main = do print foo -- B1 설정 여부 테스트: [True,False] print bar -- V 존재 여부 테스트(값 없이): [False,True] print baz -- 가능하면 V의 값 얻기: [Nothing,Just 3] foo :: [Bool] -- 인자가 없는 생성자 B1을 질의 foo = map $(getopt (THNoArg (mkArg "B1" 0))) options bar :: [Bool] -- V는 단항 생성자. mkArg가 필요한 와일드카드 패턴 "V _"를 만든다. bar = map $(getopt (THNoArg (mkArg "V" 1))) options -- 여기서는 와일드카드를 쓸 수 없다! baz :: [(Maybe Int)] baz = map $(getopt (THArg (conP "V" [varP "x"]))) options
OptionsTH.hs:
haskellmodule OptionsTH where import Language.Haskell.TH.Syntax -- 옵션 질의를 위한 데이터 타입: -- NoArg: 값에 관심 없음(불리언 플래그 포함) -- Arg: 단항(!) 생성자의 값을 가져옴 data Args = THNoArg Pat | THArg Pat getopt :: Args -> ExpQ getopt (THNoArg pat) = lamE [varP "y"] (caseE (varE "y") [cons0, cons1]) where cons0 = match pat (normalB [| True |]) [] cons1 = match wildP (normalB [| False |]) [] -- 나중에 쓰기 위해 "var"를 바인딩! getopt (THArg pat@(ConP _ [VarP var])) = lamE [varP "y"] (caseE (varE "y") [cons0, cons1]) where cons0 = match pat (normalB (appE [|Just|] (varE var))) [] cons1 = match wildP (normalB [|Nothing|]) [] mkArg :: String -> Int -> Pat mkArg k c = conP k (replicate c wildP)
이 예제는 불리언 옵션에 대해서는 훨씬 쉽게 테스트할 수 있었지만, 두 종류의 인자를 비슷한 방식으로 처리할 수 있음을 보여준다.
getopt (THArg pat)은 단항 생성자만 처리할 수 있다. 패턴 바인딩에서 정확히 하나의 VarP만 매칭하기 때문이다.
아래는 좀 더 줄여주지만, 내가 이 방식이 좋은지는 아직 모르겠다. c가 0 또는 1일 때만 동작한다.
haskellmkArg k c = conP k (replicate c (varP "x")) baz = map $(getopt (THArg (mkArg "V" 1))) -- VolkerStolz
다음처럼 길이가 서로 다른 레코드 타입을 많이 가지고 있다고 하자:
haskelldata PGD = PGD { pgdXUnitBase :: !Word8, pgdYUnitBase :: !Word8, pgdXLUnitsperUnitBase :: !Word16 }
현재는 GHC의 Binary 모듈을 사용해 파일에서 읽는다. 이 모듈은 (Word8, (Word8, Word16)) 같은 타입은 처리할 수 있지만, 각 요소를 자동으로 가져오기 위해 필요한 uncurry 호출 수를 올바르게 생성하는 쉬운 방법은 없었다.
Template Haskell로 이제 인스턴스 선언을 이렇게 쓴다:
haskellinstance Binary PGD where get bh = do a <- get bh ; return $ $(constrRecord PGD) a
여기서 핵심은 constrRecord이며 정의는 다음과 같다:
haskellconstrRecord x = reify exp where reify = \(Just r) -> appE r $ conE $ last args exp = foldl (dot) uncur $ replicate terms uncur terms = ((length args) `div` 2) - 2 dot x y = (Just $ infixE x (varE ".") y) uncur = (Just [|uncurry|]) args = words . show $ typeOf x -- AutrijusTang
여기서 $(zipn 3) = zipWith3 등이다.
haskellimport Language.Haskell.TH; import Control.Applicative; import Control.Monad zipn n = do vs <- replicateM n (newName "vs") [| \f -> $(lamE (map varP vs) [| getZipList $ $(foldl (\a b -> [| $a <*> $b |]) [| pure f |] (map (\v -> [| ZipList $(varE v) |]) vs)) |]) |]
zipWith를 거의 모든 데이터에 대해 일반화한 것. TH 스플라이스에서 동적 바인딩(‘dyn’)을 할 수 있음을 보여준다.
haskellzipCons :: Name -> Int -> [String] -> ExpQ zipCons tyName ways functions = do let countFields :: Con -> (Name,Int) countFields x = case x of NormalC n (length -> fields) -> (n, fields) RecC n (length -> fields) -> (n,fields) InfixC _ n _ -> (n,2) ForallC _ _ ct -> countFields ct TyConI (DataD _ _ _ [countFields -> (c,n)] _) <- reify tyName when (n /= length functions) $ fail "wrong number of functions named" vs <- replicateM ways $ replicateM n $ newName "x" lamE (map (conP c . map varP) vs) $ foldl (\con (vs,f) -> con `appE` foldl appE (dyn f) (map varE vs)) (conE c) (transpose vs `zip` functions)
이 예제는 표현식이 스플라이스될 때 스코프에 있는 어떤 +든 사용한다:
text:type $(zipCons ''(,,,) 2 (replicate 4 "+")) $(zipCons ''(,,,) 2 (replicate 4 "+")) :: (Num t, Num t1, Num t2, Num t3) => (t, t1, t2, t3) -> (t, t1, t2, t3) -> (t, t1, t2, t3)
‘deriving 함수’를 사용해 타입의 각 생성자마다 메서드 인스턴스를 생성하는 예제. deriving 함수는 메서드 본문을 제공한다.
이 예제는 클래스의 함수들이 인스턴스가 매개변수화된 것과 동일한 타입의 파라미터를 받는다고 가정한다.
해당 이메일 메시지에 전체 소스가 포함되어 있다(추출된 파일).
ghc-6.10의 새 기능인 -XQuasiQuotes는 라이브러리 코드에서 GHC의 구문을 확장할 수 있게 해준다. 꽤 많은 예제가 예전의 haskell-src-meta 일부였다. 이 중 일부는 이제 applicative-quoters의 일부다.
표현식 컨텍스트에서 사용되는(quasiquoter의 _quoteExp_를 쓰는) Quasiquoter는 대략 정규 TH 스플라이스와 비슷하게 동작한다:
haskellsimpleQQ = QuasiQuoter { quoteExp = stringE } -- 다른 모듈에 있음 [$simpleQQ| a b c d |] == $(quoteExp simpleQQ " a b c d ")
이 예제는 큰 데이터 타입을 다루는 고통을 줄이기 위해 Scrap your boilerplate (SYB)를 사용한다. <datatype>이 주어지면, <datatype>_opt라는 새 데이터 타입을 생성하는 mkOptional을 정의한다. 이 새 타입은 <datatype>과 유사하지만, 각 레코드 필드 <field>:: <type>은 <field>_opt:: Maybe <type>로 바뀌며, 또한 같은 모듈에 정의된 모든 <type>은 <type>_opt로 치환된다.
먼저 mkOptional 사용 예:
haskell{-# LANGUAGE TemplateHaskell #-} import A data Foo = Foo { a :: Double , f :: Int } data Bar = Bar { x :: Foo , y :: String } mapM mkOptional [''Foo, ''Bar]
위 코드는 다음의 새로운 타입 Foo_opt, Bar_opt를 생성한다:
haskelldata Foo_opt = Foo_opt { a_opt :: Maybe Double , f_opt :: Maybe Int } data Bar_opt = Bar_opt { x_opt :: Maybe Foo_opt , y_opt :: Maybe String }
x_opt의 타입이 Maybe Foo_opt이고 Maybe Foo가 아님에 주목하라.
mkOptional 구현은 다음과 같다:
haskell{-# LANGUAGE ScopedTypeVariables, TemplateHaskell #-} module A where import Language.Haskell.TH import Data.Generics addMaybesAndOpts :: Maybe String -> Dec -> Q Dec addMaybesAndOpts modName dec = -- 선언 @dec@ 전체에 @rename@과 @addMaybe@를 적용한다. -- -- SYB의 @everywhere (mkT (f :: a -> a)) (x :: b)@는 -- @x@ 안의 @a@ 타입인 모든 데이터에 @f@를 적용한다. -- @everywhereM (mkM (f :: a -> m a) (x :: b)@도 비슷하지만, -- 모나딕하게 @x@ 전체에 @f@를 적용한다. everywhere (mkT rename) <$> everywhereM (mkM addMaybe) dec where -- 주어진 모듈에서 온 이름이면 "_opt" 접미사를 붙인다. rename :: Name -> Name rename n = if nameModule n == modName then mkName $ nameBase n ++ "_opt" else n -- 레코드 필드의 타입을 @Maybe@로 감싼다. addMaybe :: (Name, Strict, Type) -> Q (Name, Strict, Type) addMaybe (n, s, ty) = do ty' <- [t| Maybe $(return ty) |] return (n,s,ty') mkOptional :: Name -> Q Dec mkOptional n = do TyConI dec <- reify n addMaybesAndOpts (nameModule n) dec