[하스켈] GADT(Generalized Algebraic Data Type) 사용하기 (2)
이전 글보기
[하스켈] GADT(Generalized Algebraic Data Type) 사용하기 (1)
5. GADT 와 ADT 의 차이점 정리
(1) 사실은 ...
GADT 는 ADT 보다 확장된 자료형입니다.
그러므로 ADT 에서 가능한 것은 GADT 에서 모두 가능합니다.
예제 1은 사실 ADT 와 GADT 둘 다 가능한 예제입니다.
instance Show SKI where show (S) = "S" show (K) = "K" show (I) = "I" (:@) :: SKI -> SKI -> SKI
부분을 data SKI = S | K | I | SKI :@ SKI 로 바꾸면 완벽히 똑같이 작동됩니다.
그리고 앞에서 컴파일 오류가 발생했던 다음의 코드도
-- ADT 문법 (컴파일 오류 발생) data ABC x = A a | B (a -> b)
다음과 같이 forall 을 이용하여 다음과 같이 바꾸면 제대로 동작합니다.
data ABC x = forall a. A a | forall a b. B (a -> b)
여기까지의 설명은 ADT 와 GADT 의 문법상의 차이점에 대해서 알아본 것이었습니다.
그러면 이제 본격적으로 GADT 만의 기능에 대해 알아봅시다.
(2) GADT 와 ADT 의 결정적인 차이점
GADT 와 ADT 의 차이점을 한마디로 말하면 다음과 같습니다.
GADT 는, 생성자의 반환형이 여러가지일 수 있다.
그렇기 때문에 GADT 의 형 내부에 또 다른 형 관련정보를 넣을 수 있습니다.
예를들어, 다음과 같이 내부의 자료가 Int 형인지 Char 형인지에 따라,
생성자의 반환형이 각각 T Int 와 T Char 이 되게 만들면,
-- t.hs 로 저장함. module T where data T x where IntT :: Int -> T Int CharT :: Char -> T Char
Int 인지 아니면 Char 인지에 대한 정보가 x 에 들어있으므로,
이를 이용하면 반환형이 여러가지인 함수를 정의할 수 있습니다.
eval :: T x -> x eval e = case e of IntT i -> i CharT c -> c
Prelude> :l t.hs [1 of 1] Compiling T ( t.hs, interpreted ) Ok, modules loaded: T. *T> eval (IntT 1) 1 *T> eval (CharT 'a') 'a'
그리고 GADT 의 기능을 사용한 경우,
하스켈의 형 유추(type inference)가 잘 이뤄지지 않기 때문에, 함수 선언은 반드시 해줘야합니다.
6. 예제 2 : 리스트? 튜플?
(1) 리스트와 튜플
리스트와 튜플에 대해서는 잘 아실겁니다. 간단하게 둘을 비교해보면 다음과 같습니다.
튜플에는, `다양한 형을 가진 자료'가 `고정된 수'만큼 있습니다.
리스트에는, `동일한 형을 가진 자료'가 `임의의 수'만큼 있습니다.
리스트에는 `동일한 형을 가진 자료'만 올 수 있다는 점은 (:) 생성자의 형을 보면 더 명확해집니다.
(이제서야 밝히지만 생성자도 함수의 한 종류이고, (알파벳이나 _ 가 아닌) 특수문자만으로 이름붙은 함수를 연산자라고 부릅니다.
그러므로 (:) 는 생성자이자 함수이고 연산자입니다. 물론, 보통은 연산자라고 부릅니다.
참조 : http://www.haskell.org/tutorial/functions.html , http://www.haskell.org/haskellwiki/Infix_operator)
Prelude> :t (:) (:) :: forall a. a -> [a] -> [a]
리스트를 흉내내서 List 라는 자료형을 정의해봅시다.
(하스켈에서 리스트는 원래 모나드이지만, 본 문서에서 그건 신경쓰지 않겠습니다.)
-- ADT 문법 data List a = Nil | a :@ List a
-- GADT 문법 data List a where Nil :: List a (:@) :: a -> List a
*Main> :t (:@) (:@) :: forall a. a -> List a
그러면 다음과 같이 (:@) 생성자의 형을 바꾸면 어떨까요?
(:@) :: forall a. a -> [ b ] -> [ a -> b ]
(2) 일반화된 리스트 (Generalized List)
리스트를 확장하여 GL 이라는 자료형을 만들어봅시다.
GL 는 `다양한 형을 가진 자료'가 `임의의 수'만큼 있는 자료형입니다.
-- list.hs 로 저장합니다. module GL where data GL x where Nil :: GL () (:@) :: a -> GL b -> GL (a -> b) infixr 5 :@
잘 정의됐나 확인해봅시다.
Prelude> :l list.hs [1 of 1] Compiling GL ( list.hs, interpreted ) Ok, modules loaded: GL. *GL> :t (1 :@ 'a' :@ "string" :@ Nil) (1 :@ 'a' :@ "string" :@ Nil) :: forall t. (Num t) => GL (t -> Char -> [Char] -> ())
head, tail 과 비슷한 일을 하는 함수도 만들어 봅시다.
headGL :: GL (a -> b) -> a headGL (x :@ y) = x tailGL :: GL (a -> b) -> GL b tailGL (x :@ y) = y
실행결과는 다음과 같습니다.
*GL> headGL (1 :@ 'a' :@ "string" :@ Nil) 1 *GL> :t tailGL (1 :@ 'a' :@ "string" :@ Nil) tailGL (1 :@ 'a' :@ "string" :@ Nil) :: GL (Char -> [Char] -> ())
하지만 일반화되었다고 무조건 좋은 것만은 아닙니다.
만약, 리스트를 GL 자료형으로 바꾸는 함수를 구현해야한다고 생각해봅시다.
다음과 같이 하면 될까요?
convertGL :: [a] -> GL t convertGL (x :@ xs) = x :@ (convertGL xs)
불행히도 컴파일 오류를 내버립니다.
GL 의 정의와 충돌을 일으키기 때문이데, 다음과 같이 GL 의 정의를 바꾸면,
-- list2.hs 로 저장합니다. module GL where data GL where Nil :: GL (:@) :: a -> GL -> GL infixr 5 :@ convert :: [a] -> GL convert [] = Nil convert (x:xs) = x :@ (convert xs)
다음과 같이 잘 되는 것 같이 보이지만,
Prelude> :l list2.hs [1 of 1] Compiling GL ( list2.hs, interpreted ) Ok, modules loaded: GL. *GL> :t convert [1,2,3] convert [1,2,3] :: GL *GL> :t 'a' :@ (convert [1,2,3]) 'a' :@ (convert [1,2,3]) :: GL
이번에는 headGL 함수에서 오류가 발생합니다.
-- 컴파일 오류 발생 headGL :: GL -> a headGL (x :@ y) = x
(3) GADT 의 이용
앞에서 언급했듯이, GADT 의 형 내부에 또 다른 형 관련정보를 넣을 수 있습니다.
그리고 이러한 형 관련정보는, 하스켈의 형 시스템에 의해서, 자료와 함수의 형을 확인하기 위해서 활용됩니다.
그렇기 때문에 GADT 를 이용하면, 오히려 함수를 정의하는데 걸림돌로 작용합니다.
리스트에 다양한 자료형을 넣고 싶다면, 다음과 같이 사용하는 편이 더 낫습니다.
module T where data T = BoolT Bool | IntT Int | CharT Char instance Show T where show (BoolT x) = show x show (IntT x) = show x show (CharT x) = show x int2t :: [Int] -> [T] int2t [] = [] int2t (x:xs) = (IntT x) : (int2t xs) grepInt :: [T] -> [Int] grepInt [] = [] grepInt (x:xs) = case x of IntT x' -> x' : (grepInt xs) _ -> grepInt xs
7. 정리
GADT 는, 생성자의 반환형이 여러가지일 수 있습니다.
이러한 특성과 함께 함수의 다형성(Polymorphism)을 같이 이용하면, 여러가지 반환형을 갖는 함수를 만들어낼 수 있습니다.
이러한 반환형은, 하스켈 컴파일러에 의해서, 함수나 각종 연산의 형을 확인하는데 사용됩니다.
그러므로 하스켈에서 GADT 는, 하스켈 언어가 `강형(strong typing)'이라는 특성과 맞물려서,
하스켈의 형 시스템을 피하기 위한 수단으로 사용되기란 힘들고,
컴파일 시점에 컴파일러에게 더 많은 형 관련정보를 넘겨주기 위한 용도로 사용하는 것이 자연스럽습니다.
이것으로 GADT 에 대한 설명은 끝났습니다.
하스켈 위키(http://www.haskell.org/haskellwiki/GADT)의 예제를 찾아서 GADT 가 어떻게 사용되었는지도 살펴보시기 바랍니다.
---------------------------------------------------------------------------------------------------
Copyright (c) redneval@kldp.org, 2009.
GNU 자유 문서 라이선스 1.3판 또는 자유 소프트웨어 재단에서 발행한 이후 판의 규정에 따라,
본 문서를 복제하거나 개작 및배포할 수 있습니다.
본 문서에는 변경불가부분과 앞 표지구절 및 뒷 표지구절이 없습니다.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License,
Version 1.3 or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts and no Back-Cover Texts.
---------------------------------------------------------------------------------------------------
댓글 달기