CODEONWORT

newtype 선언 본문

Season 1/하스켈

newtype 선언

codeonwort 2014.09.29 11:40
하스켈에서 타입을 선언하는 키워드는 data, type, newtype 이렇게 세 개다. 그런데 위키책에서 newtype을 나중에 언급한다고 해놓고는 Beginner's Track이 끝날 때까지 안 해서 HaskellWiki에 있는 newtype 항목만 여기에 따로 번역한다.


Newtype

newtype 선언은 data와 매우 비슷한 방식으로 새로운 타입을 생성한다. newtype의 문법과 용법은 data 선언과 거의 같다. 사실은 newtype 키워드를 data로 교체해도 컴파일이 되고 심지어 프로그램은 대부분 잘 돌아갈 것이다. 하지만 그 반대는 아니다. data를 newtype으로 교체할 수 있는 것은 생성자가 오직 하나고 그 생성자 내의 필드가 오직 하나일 때 뿐이다.

몇 가지 예시:

newtype Fd = Fd CInt
-- data Fd = Fd CInt도 타당하다

-- newtype도 일반 타입처럼 파생절을 가질 수 있다
newtype Identity a = Identity a
  deriving (Eq, Ord, Read, Show)

-- 레코드 문법도 가능하지만 하나의 필드만 허용된다
newtype State s a = State { runState :: s -> (s, a) }

-- 이것은 허용되지 *않는다*
-- newtype Pair a b = Pair { pairFst :: a, pairSnd :: b }
-- 하지만 이건 허용된다
data Pair a b = Pair { pairFst :: a, pairSnd :: b}
-- 이것도 된다
newtype Pair' a b = Pair' (a, b)

이렇게 제한되서야 newtype을 어디에 써먹으라는 걸까?

목차
1 단축 버전
2 골치아픈 부분
3 strict 타입에 대해서는?
4 예시
5 더 보기

1 단축 버전
필드가 하나인 생성자 하나라는 제한은 이 새로운 타입과 그 필드의 타입이 직접 대응 관계임을 뜻한다.

State :: (s -> (s, a)) -> State s a
runState :: State s a -> (s -> (s, a))
 
수학적으로 말하자면 이것들은 동형isomorphic이다. 컴파일 시간에 타입이 검사되고 나면 런타임에는 두 타입을 같은 것으로 다룰 수 있어서 통상 데이터 생성자와 연관되는 오버헤드나 간접 참조(indirection)가 없다. 따라서 특정 타입에 대해 여러 타입 클래스를 선언하려거나 어떤 타입을 추상 타입으로 만들고 싶다면, 그 타입을 newtype으로 감싸면 타입 검사기는 이를 별개의 것으로 다루지만 런타임에는 동일한 것이 된다. 그러면 GHC가 바이트 덩어리들을 이리저리 움직이는 것을 걱정할 필요 없이 phantom type이나 recursive type 같은 온갖 기교를 부릴 수 있다.


2 골치아픈 부분

그럼 왜 모두가 newtype을 쓰지 않는 걸까? 음, 대부분은 그렇게 한다. 하지만 미묘하지만 문법상 중요한 차이가 있다. Bool과 동형인 데이터 타입을 만들 때

data Any = Any { getAny :: Bool }


이 동형성이라는 것이 정확하지 않다는 것을 알게 된다.

Any . getAny $ Any True  = Any True  -- 괜찮다
Any . getAny $ Any False = Any False -- 이것도 괜찮다
Any . getAny $ Any ⊥     = Any ⊥    -- 아직까진 좋다
Any . getAny $ ⊥         = Any ⊥    -- 잠깐만...
 

문제는 data 키워드로 선언한 타입들이 lift된다는 것이다. 즉 그것들은 다른 모든 것과 구분되는 별개의 ⊥ 값을 포함한다. 이 예에서는 Any ⊥ :: Any와 다른 ⊥ :: Any가 있다. 이것이 뜻하는 바는 다음의 패턴 비교가

case x of
  Any _ -> ()
 
그 인자를 반드시 평가해봐야 한다는 것이다. 패턴 비교가 실패할 리 없는 것 같아 보여도 말이다. 우리는 x가 ⊥인지 어떤 y에 대해 Any y인지 검사해야 한다. 이것이 하스켈의 게으르고 모든 것을 처리하지 않는 시맨틱의 본질이다. 문제는 이러면 어떤 값이 생성자에 의해 감싸지는지 아닌지 추적해야 하고, 이것이 뜻하는 바는 우리가 원하지도 않은 여분의 bottom value를 구별하기 위해서라도 런타임에 여분의 생성자를 추적해야 한다는 것이다. 그러니 일관되게 하면서도 정확한 동형성을 보존하기 위해 하스켈은 lift되지 않는 타입의 생성을 위해 newtype 키워드를 제공하는 것이다. newtype 생성자에 대한 패턴 매칭은 먹히지 않는데 별개의 ⊥이 없어서 타입 내의 모든 값이 생성자에 의해 감싸지기 때문이다.


3 strict 타입에 대해서는?

알아챘을지 모르겠는데 이런 타입에서

data Identity' a = Identity' !a

 
Identity' ⊥ = ⊥ 이고 따라서 그 동형성이란 걸 얻은 게 아닌지 생각할 수도 있다. 하지만 모든 적극성 표기strictness annotation가 뜻하는 것은 Identity' ⊥ 가 실제로는 Identity' $! ⊥ 을 뜻한다는 것이다. 타입의 시맨틱은 같지만 특히 case 표현식이 여전히 그 값을 강제한다.


4 예제
module Foo where
 
data Foo1 = Foo1 Int    -- Int를 느슨하게 참조하는 생성자 Foo1을 정의한다
data Foo2 = Foo2 !Int   -- Int를 적극적으로 참조하는 생성자 Foo2를 정의한다
newtype Foo3 = Foo3 Int -- Int와 같은 뜻을 갖는 Foo3 생성자를 정의한다
 
-- 인수는 게으르고 무시된다. 따라서 undefined는 실패를 일으키지 않는데
-- 생성자 패턴 비교가 성공하기 때문이다. 
x1 = case Foo1 undefined of
     Foo1 _ -> 1 -- 1
 
-- 인자는 적극적이고(! 때문) 따라서 undefined는 실패를 유발한다 .
x2 = case Foo2 undefined of
     Foo2 _ -> 1 -- undefined

-- newtype은 Int처럼 행동한다. 밑의 yInt를 볼 것
x3 = case Foo3 undefined of
     Foo3 _ -> 1 -- 1
 
-- 생성자 패턴 비교가 실패한다
y1 = case undefined of
     Foo1 _ -> 1 -- undefined
 
-- 생성자 패턴 비교가 실패한다
y2 = case undefined of
     Foo2 _ -> 1 -- undefined
 
-- newtype은 Int처럼 행동한다. 런타임에는 아무 생성자도 없다.
y3 = case undefined of
     Foo3 _ -> 1 -- 1

-- Int의 행동에 관한 예시
int :: Int
int = undefined
 
yInt = case int of
       _ -> 1                   -- 1


5 더 보기
Haskell 98 Report는 4.2.3절에서 newtype을 정의한다.
0 Comments
댓글쓰기 폼