확장 TypeFamilies - 타입 함수

Posted on August 19, 2020

타입 함수

하스켈은 엄격하게 타입을 검사하는 언어인데, 폴리모픽하게 함수들을 만들다 보면, 프로그래머가 명시적으로 지정하지 않아도 하스켈이 때에 따라 타입 추론을 달리 했으면 할 때가 있습니다. 한 두개의 타입만 정해지면 함수에서 쓰이는 여러 타입들을 알아서 적당한 타입으로 바꿔 주는게 필요합니다.

예를 들면 다음처럼 인스턴스에 따라 method의 결과 타입이 다른 게 필요할 수 있습니다.

instance SomeClass TypeA where...
  method :: TypeA -> Int

instance SomeClass TypeB where...
  method :: TypeB -> Double

좀 더 추상적으로 얘기하면 “타입에 따라 타입이 결정”되는 기능이 필요합니다. TypeA일 때는 Int, TypeB일 때는 Double. 값에 따라 값이 결정되는 걸 함수라고 합니다. 타입에 따라 타입이 결정되니, 타입 레벨 프로그래밍에서 타입 함수로 쓰입니다.

Type Synonym Family

일반 type 구문과 마찬가지로 새 타입을 만들진 않습니다. 때에 따라 다른 것과 연결될 수 있는 별명을 만든다고 볼 수 있습니다.

class 내부에서 쓰일 수도 있고,(스탠다드 하스켈에서는 class 안에서 type 선언이 쓰이지 않으므로, class 안에서 family를 선언할 땐 family를 빼도 확장으로 다룹니다.)

class SomeClass a b where
  type VarType a b
  method :: a -> b -> VarType a b

instance SomeClass A B where
  type VarType A B = B
-- 보통 인스턴스에서 타입 선언을 하진 않습니다. (instanceSigs 확장 참고)
-- Type Family를 쓰면 아래 처럼 선언한 것과 같은 상황이 됩니다.
  method :: A -> B -> B

instance SomeClass B A where
  type VarType B A = A
  method :: B -> A -> A

단독으로 쓰일 수도 있습니다.(일반 type선언과 구별하기 위해 family를 꼭 써줘야 합니다.)

type family VarType a b where
  VarType A B = B
  VarType B A = A

-- 위와 같이 한 곳에서 정의할 수도 있고,
-- 아래 같이 instance 구문을 써서 정의할 수도 있습니다.
-- instance를 쓰면, 한 곳에서 한 번에 정의할 필요가 없어 좀 더 유연하게 쓸 수 있습니다.

type family VarType a b :: *
type instance VarType A B = B
type instance VarType B A = A

클래스 내부에서 쓰든 단독으로 쓰든, 타입 패밀리로 정의해 두면, 코드 중에 나오는 VarType a b 타입은 GHC가 알아서 실제 타입으로 바꿉니다.

Data Family

위와 다르게 타입 인자에 따라 새로운 타입을 만듭니다.

data family XList a
data instance XList Char = XCons !Char !(XList Char) | XNil
data instance XList () = XListUnit !Int

마찬가지로 코드 중에 XList a를 만나면 a에 따라 GHC가 실제 타입으로 바꿉니다.

패밀리의 인스턴스로 선언한다고 코드 조립할 때 패밀리끼리 어떤 연관성을 가지는 건 아닙니다. 조립할 때, 패밀리 중 하나의 타입으로 결정됩니다. 결국 인스턴스들은 모두 다른 타입입니다. 그래서 아래같이 값생성자 패턴 매칭을 할 수 없습니다.

data family T a
data instance T Int = A
data instance T Char = B
foo :: T a -> Int
foo A = 1 -- 에러
foo B = 2 -- 에러

foo는 타입에 따라 다른 동작을 합니다. 타입에 따라 다른 동작을 하려면? 맞습니다. 클래스와 인스턴스를 이용하면 됩니다.

class Foo a where
  foo :: T a -> Int
instance Foo Int where
  foo A = 1
instance Foo Char where
  foo B = 2

동일한 표현을 GADT 할 수도 있는데, 차이점은 Type Family는 열려 있는 상태라 언제든지 instance를 추가할 수 있습니다. 다른 모듈에서 추가한다든지 하는 것도 가능합니다.

functional dependencies로 구현한 것들을 type family로 표현할 수도 있습니다.

참고
http://cdepillabout.github.io/haskell-type-families-presentation/index-en.html#/
https://www.servant.dev/posts/2018-07-12-servant-dsl-typelevel.html
https://wiki.haskell.org/GHC/Type_families
http://www.mchaver.com/posts/2017-06-21-type-families.html

Initial 인코딩 AST를 대수 타입으로 표현하는 것 데이터를 initial 대수로 보는 것. initial 대수는 카테고리 이론 용어

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