프로그래밍 흑마법: TemplateHaskell - 2

Eunmin Kim·2022년 6월 22일
1

하스켈 일기

목록 보기
11/12
post-thumbnail

어제 시간이 없어서 완성하지 못한 Bson Val 타입 클래스 인스턴스를 생성하는 템플릿을 완성했다. 예상하지 못했지만 GitHub Copilot의 도움을 많이 받았다. 무료 사용 기간이지만 큰 도움을 받았고 스택 오버 플로우 검색 시간을 줄여 삽질 시간을 많이 아낄 수 있었다. 무료 기간이 끝나도 결제를 할 것 같다. 물론 완벽한 코드를 만들어 주지 않았지만 충분히 도움이 될 만한 예제 코드를 생성해줬고 좋은 경험이었다.

어쨌든 똑똑한 인공 지능에게 도움을 받아 다음과 같이 어떤 타입 레코드 타입(범용적이지는 않다)의 Bson Val 인스턴스를 만들어주는 함수를 만들 수 있었다. (코드가 깔끔하지는 않다)

deriveVal :: Name -> Q [Dec]
deriveVal name = do
    TyConI (DataD _ _ _ _ [RecC con fields'] _) <- reify name
    let fields = [ field | (field, _, _) <- fields' ]
    varName <- newName "value"
    [d|
        instance Val $(conT name) where
            val $(varP varName) = Doc $(listE $ map (makeField varName) fields)
            cast' = undefined
        |]
    where
        makeField varName fieldName =
            [| $(litE $ stringL (nameBase fieldName))
                =: $(appE (varE fieldName) (unboundVarE varName)) |]

몇 가지 새로운 사실을 알았다. 옥스퍼드 괄호는 중첩해서 사용할 수 없다는 점과 newName으로 임의의 변수명을 생성할 수 있다는 점이다. 템플릿 코드에서 사용할 변수명을 생성하는 것은 Clojure에도 있다. gensym이라는 함수인데 사용법과 목적이 같아서 이해하기 쉬웠다. 그리고 코드 생성 타임에 실행되는 코드와 실제 런타임에 실행되는 코드의 컨텍스트 전환도 중요했는데 역시 Clojure에서 매크로를 많이 만들어 본 것이 도움이 되었다. 어떤 언어에서든 메타 프로그래밍에 익숙해지면 다른 언어에서도 잘할 수 있는 것 같다.

잘 동작하는지 확인해보면 다음과 같다.

data User = User
    { userId   :: Int
    , userName :: String
    } deriving (Eq, Show)

$(deriveVal ''User)

-- >>> val $ User 1 "dqwdqwd"
-- [ userId: 1, userName: "dqwdqwd"]

data Invitation = Invitation
    { invitationId     :: Int
    , invitaitonFromId :: Int
    } deriving (Eq, Show)


$(deriveVal ''Invitation)

-- >>> val $ Invitation 1 2
-- [ invitationId: 1, invitaitonFromId: 2]

잘 동작한다. Generic 없이도 Val 인스턴스를 잘 만들 수 있다. 핵심적인 부분은 Language.Haskell.TH.Syntax 모듈에 있는 reify이다. reifyName을 받아 Q 컨택스트에서 Info 값을 가져올 수 있는데 Name 값이 어떤 타입 이름이라면 Info에는 해당 타입에 대한 여러 가지 정보를 얻을 수 있다. 그래서 필드명 같은 것을 가져올 수 있었다.

완성하고 몇 가지 함정을 알 수 있었는데 deriveVal가 생성하는 코드가 Data.Bson 모듈에 의존적이기 때문에 Data.Bson 모듈을 import 해줘야 한드는 점과 사용하는 곳에 언어 Data.Bson에 의존적인 언어 확장을 추가해줘야 한다는 점이다. 지금 사용하고 있는 GraphQL 라이브러리인 morpheus-graphql 도 생성하는 코드에 필요한 언어 확장과 의존적인 import를 해줘야 하는 것을 보면 쉽게 해결할 수 있는 문제는 아닌 것 같다.

TemplateHaskell 맛보기 일기의 마지막 문구는 다른 곳에서도 많이 경고하고 있듯이 가능한 TemplateHaskell를 사용하지 않는 것을 권하는 것으로 마치려고 한다. 만약 언어 버전이 바뀌거나 언어 외부는 안 바뀌더라도 언어 내부의 AST 구조가 바뀐다면 기존에 만들어 둔 TemplateHaskell 함수는 동작하지 않을 수도 있는 위험이 있다. 또 모든 메타 프로그래밍처럼 디버깅이 어렵다는 문제도 있다.

라이브러리를 만드는 것이 아니라면 가능한 TemplateHaskell 사용하지 맙시다. :)

profile
Functional Programmer @Constacts, Inc.

1개의 댓글

comment-user-thumbnail
2023년 1월 15일

https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/meta-haskell.pdf
위 논문에 아래와 같은 소스가 있습니다.

gen (D : xs) x = [| \n-> $(gen xs [| $x++show n |]) |]
gen (S : xs) x = [| \s-> $(gen xs [| $x++s |]) |]

옥스포드 괄호의 중첩이 혹시 이 걸 말씀하신 걸까요?

답글 달기