계정: 로그인
AA 📝
6. 다시 한 번 타입

A Gentle Introduction to Haskell, Version 98
이전 다음 위로


타입 선언에 대해서 좀 더 심도있게 다뤄봅시다.

6.1 Newtype 선언

프로그래밍에서 흔한 일 중 하나가 바로 이미 존재하는 타입과 표현은 같고 타입 시스템 상에서의 identity는 다른 새로운 타입을 정의하는 일입니다. Haskell에서는 newtype 선언이 이미 존재하는 타입으로부터 새로운 타입을 만들어줍니다. 예를 들어, 자연수는 아래와 같은 선언을 사용해서 Integer 타입으로부터 표현될 수 있습니다:

   1 newtype  Natural    =  MakeNatural Integer

상기 선언은 구성자가 단지 Integer 하나만 포함하는 완전히 새로운 타입 Natural을 만듭니다. 구성자 MakeNaturalIntegerNatural 사이를 변환시킵니다:

   1 toNatural                  :: Integer -> Natural
   2 toNatural x | x < 0        =  error "Can't create negative naturals!" 
   3             | otherwise    =  MakeNatural x
   4 
   5 fromNatural                    :: Natural -> Integer
   6 fromNatural (MakeNatural i)    =  i

아래 인스턴스 선언은 NaturalNum 클래스에 포함시킵니다:

   1 instance  Num Natural  where
   2     fromInteger    =  toNatural
   3     x + y          =  toNatural (fromNatural x + fromNatural y)
   4     x - y          =  let r = fromNatural x - fromNatural y in
   5                       if r < 0 then error "Unnatural subtraction"
   6                                else toNatural r
   7     x * y          =  toNatural (fromNatural x * fromNatural y)

이런 선언이 없으면 NaturalNum에 포함될 수 없습니다. 원래의 타입에 대해 선언된 인스턴스는 새로운 타입에까지 전파되지 않습니다. 사실, 이런 타입의 목적은 Num의 전혀 다른 인스턴스를 소개하려는 것입니다. 이것은 만일 NaturalInteger의 타입 동의어로 정의된다면 불가능한 일입니다.

이런 일들은 newtype 선언 대신 data 선언을 사용해도 모두 동작합니다. 그러나, data 선언은 Natural의 값을 표현하는 데에 추가적인 부담이 걸리게 됩니다. newtype을 사용하면 data 선언이 발생시키는 추가적인 수준의 (laziness로 인한) 간접성을 피할 수 있습니다. newtype, data, 그리고 type 선언문의 관계에 대한 더 자세한 논의는 Haskell Report의 4.2.3절참조하시기 바랍니다.

키워드만 제외하면, newtype 선언문은 하나의 필드만을 포함하는 하나의 구성자를 갖는 data 선언문과 같은 구문을 사용합니다. newtype을 사용해서 정의한 타입은 보통의 data 선언을 통해 만들어진 타입들과 거의 동일하기 때문에, 이런 구문은 적절한 것입니다.

6.2 필드 레이블 (Field Labels)

Haskell 자료형에 있는 필드는 위치적으로도 접근할 수 있고 필드 레이블을 사용하는 이름으로도 접근할 수 있습니다. 2차원 좌표를 위한 자료형을 생각해봅시다:

   1 data  Point    =  Pt Float Float

Point의 두 가지 구성 요소는 구성자 Pt의 첫번째와 두번째 인수입니다. 아래와 같은 함수는:

   1 pointx             :: Point -> Float
   2 pointx (Pt x _)    =  x

좌표의 첫번째 구성 요소를 좀 더 서술적으로 참조하는 데에 사용될 수 있지만, 구조체가 커지면 이런 함수를 일일이 손으로 작성하는 것은 지루한 일이 될 것입니다.

data 선언에 들어있는 구성자들은 중괄호로 감싼 관련 필드명으로 선언할 수 있습니다. 이런 필드명들은 구성자의 구성 요소를 위치가 아닌 이름으로 구별해줍니다. 아래는 Point를 정의하는 또다른 방법입니다:

   1 data  Point    =  Pt {pointx, pointy :: Float}

이 자료형은 이전에 정의했던 Point와 동일합니다. 구성자 Pt는 두 가지 경우 모두 같은 것입니다. 그러나, 이번 선언은 pointxpointy라는 두 가지 필드명도 정의하고 있습니다. 이런 필드명들은 구조체로부터 구성 요소를 뽑아내는 선택자 (selector) 함수로 사용할 수 있습니다. 이 예제에서 선택자는 다음과 같습니다:

다음은 이런 선택자들을 사용하는 함수입니다:

   1 absPoint      :: Point -> Float
   2 absPoint p    =  sqrt (pointx p * pointx p + 
   3                        pointy p * pointy p)

필드 레이블은 새로운 값을 구성하는 데에도 사용될 수 있습니다. Pt {pointx=1, pointy=2}라는 표현식은 Pt 1 2와 동일합니다. 자료 구성자 선언에 필드명을 사용하는 것은 필드에 접근하는 위치적 방식을 저해하지 않습니다. 즉, Pt {pointx=1, pointy=2}Pt 1 2 두 가지 모두 허용됩니다. 필드명을 써서 값을 구성할 때에는 일부 필드가 생략될 수 있으며, 이렇게 생략된 필드는 정의되지 않습니다.

필드명을 이용한 패턴 매칭은 구성자 Pt의 구문과 비슷한 구문을 사용합니다:

   1 absPoint (Pt {pointx = x, pointy = y})    =  sqrt (x*x + y*y)

Update 함수는 새로운 구조체의 구성 요소를 채우기 위해 이미 존재하는 구초제의 필드 값을 사용합니다. 만일 pPoint의 일종이라면 p {pointx=2}ppointy는 갖고 pointx2로 치환된 좌표입니다. 이것은 파괴적인 갱신이 아닙니다: 갱신 함수는 단지 객체의 지정된 필드에 새 값을 채운 새로운 복사본을 하나 만들어낼 뿐입니다.

필드 레이블과 함께 사용된 중괄호는 조금 특별합니다: Haskell 구문은 일반적으로 (4.6절에 설명했던) 레이아웃 규칙을 사용함으로써 중괄호를 생략할 수 있게끔 하고 있습니다. 그러나, 필드명에 연계되어있는 중괄호는 반드시 명시적으로 적어주어야만 합니다.

필드명은 단 하나의 구성자만을 갖는 (흔히 ‘레코드’ 타입이라 일컫는) 타입에만 국한되지 않습니다. 다중 구성자를 갖는 타입에서는 필드명을 사용한 선택이나 갱신 연산은 런타임에 오류를 발생시킬 수 있습니다. 이것은 함수 head가 빈 리스트에 적용될 때의 행동과 비슷합니다.

필드 레이블은 보통 변수들과 클래스 메서드들과 함께 최상위 수준의 namespace를 공유합니다. 필드명은 같은 스코프 내에 있는 둘 이상의 자료형에서 사용할 수 없습니다. 그러나, 자료형 내부에서는, 그것이 어떤 경우든 모두 같은 타이핑을 갖는 한, 둘 이상의 구성자에서 같은 필드명을 사용할 수 있습니다. 예를 들어 아래 자료형에서:

   1 data  T    =  C1 {f :: Int, g :: Float}
   2            |  C2 {f :: Int, h :: Bool}

필드명 fT 속의 두 구성자 양쪽 모두에 적용됩니다. 따라서, 만일 x의 타입이 T라면 x {f=5}T 속의 두 구성자들 중 어느쪽에 의해 만들어진 값에 대해서라도 모두 작동하게 됩니다.

필드명은 대수적 자료형의 기본적인 특성을 바꾸지 않습니다: 필드명은 자료 구조 속의 구성 요소에 위치적 방법 대신 이름으로 접근할 수 있게끔 해 주는 간편한 구문일 뿐입니다. 구성자에 대한 모든 참조들을 다 바꾸지 않고도 필드를 추가하거나 삭제할 수 있기 때문에, 필드명은 많은 구성 요소들을 갖는 복잡한 구성자들을 좀 더 다루기 쉽게 만들어줍니다. 필드 레이블과 그 의미론에 대한 상세한 내용은 §4.2.1보시기 바랍니다.

6.3 Strict Data Constructors

Haskell의 자료 구조는 일반적으로 lazy합니다: 구성 요소들은 그것이 정말로 필요해지기 전까지는 결코 평가되지 않습니다. 이런 특성은 자료 구조로 하여금 만일 평가된다면 오류가 나거나 종료되지 못할 구성 요소들을 포함할 수 있게끔 해줍니다. 게으른 자료 구조는 Haskell의 표현력을 증대시키며, Haskell 프로그래밍 스타일의 가장 골자가 되는 측면입니다.

내부적으로, 게으른 자료 객체의 각각의 필드는 흔히 thunk라 불리는 구조로 감싸져 있으며, 이 thunk는 필드 값을 정의하는 계산을 캡슐화합니다. 값이 필요해지기 전까지는 이 thunk 속으로 들어가지 않습니다: 즉, 오류(⊥)를 포함하는 thunk는 자료 구조의 다른 요소들에 영향을 미치지 않습니다. 예를 들어, Haskell에서 튜플 ('a',⊥)은 아무런 문제도 없는 완전히 적법한 값입니다. 튜플 내에 있는 다른 요소들을 전혀 방해하지 않으면서 'a'를 사용할 수 있습니다. 대부분의 프로그래밍 언어들은 lazy한게 아니라 strict합니다: 즉, 자료 구조 속의 모든 요소들은 구조 속에 위치되기 전에 값으로 reduce됩니다.

Thunk와 관련해서 몇가지 부하가 걸리는 점이 있습니다: 이들은 구성하고 평하가는 데에 시간이 걸리고, 힙 공간을 차지하며, garbage collector로 하여금 thunk를 평가하는 데에 필요한 다른 구조들을 계속 간직하게 만듭니다. 이런 부하를 피하기 위해, data 선언문 속의 strictness flags구성자의 특정 필드를 즉시, 선택적으로 (laziness를 억제하며) 평가되게 해줍니다. data 선언에서 ! 기호로 표시된 필드는 thunk 속에서 지연되는게 아니라 구조가 만들어질 때 즉시 평가됩니다. Strictness flag를 사용하기에 적절한 상황이 몇 가지 있습니다:

예를 들어, 복소수 라이브러리에는 Complex 타입이 다음과 같이 정의되어 있습니다:

   1 data  RealFloat a => Complex a    =  !a :+ !a

구성자 :+의 중위 형태 정의에 주목하십시오.

이 정의는 복소수의 두 구성 요소인 실수부와 허수부를 strict하다고 표시하고 있습니다. 이것은 복소수의 좀 더 컴팩트한 표현이지만, 대신 정의되지 않는 부분 (예를 들어 1 :+ ⊥)을 정말 전적으로 정의하지 않은 채 복소수를 만들어낸다는 희생을 감수하는 표현입니다. 부분적으로만 정의된 복소수라는건 필요 없기 때문에, 보다 효율적인 표현법을 얻기 위해서 strictness flag를 사용하는 것도 괜찮습니다.

Strictness flag는 메모리 누수 (예를 들어, 계산하는 데에 더이상 필요하지 않는데도 garbage collector가 계속 간직하고 있는 자료 구조)를 탐지하는 데에도 사용할 수 있습니다.

Strictness flag !data 선언문에만 나타날 수 있습니다. 다른 타입 지정문이나 타입 정의에는 사용될 수 없습니다. 함수의 인수를 strict하다고 표시할 수 있는 방법은 없습니다. 비록 seq!$ 같은 함수를 사용해서 같은 효과를 얻을 수는 있지만 말입니다. 더 자세한 사항은 §4.2.1를 보시기 바랍니다.

Strictness flag를 사용하는 정확한 가이드라인을 제시하긴 어렵습니다. 이들은 매우 조심스럽게 사용해야 합니다: Laziness는 Haskell의 근본적인 성질 중 하나고, 여기에 strictness flag를 추가하는 것은 무한 루프나 다른 비상 상황에 빠지는 것을 찾아내기 어렵게 만듭니다.


이전 다음 위로
A Gentle Introduction to Haskell, Version 98

Copyright © 1999 Paul Hudak, John Peterson and Joseph Fasel

Permission is hereby granted, free of charge, to any person obtaining a copy of “A Gentle Introduction to Haskell” (the Text), to deal in the Text without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Text, and to permit persons to whom the Text is furnished to do so, subject to the following condition: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Text.