[Haskell] 하스켈 기초반 5강 - 타입의 기초2
이번 5강에서는 지난번 배운 타입의 기초에
2번째 시간입니다.
이번 장에서는 숫자 타입들이 하스켈에서
어떻게 처리되는지 보여주고
타입 시스템의 몇 가지 중요한 특성을
소개하는 시간입니다.
Num 클래스
수학에는 함께 더할 수 있는 수의
종류에 몇가지 제약이 존재합니다.
예를 들어서 2 + 3(두 자연수)
(-7) + 5.12(음의 정수와 실수)
1/7 + π(유리수와 무리수)..
등이 있습니다.이것들은 모두 타당하며
사실 모든 임의의 두 실수는
덧셈이 가능합니다.
그런 일반성을 가장 단순하게
포착하려면 하스켈에서는 일반화된
Number 타입이 필요하고
그런 (+)의 시그니처는 단순히
아래와 같아야 합니다.
(+) :: Number -> Number -> Number
하지만 이런 설계는 컴퓨터가 산수를
하는 방식에 잘 들이맞지 않고
컴퓨터는 정수를 메모리 내 일련의
이진수로 다룰 수 있으나
실수에는 이 접근법이 먹히지 않습니다.
그러므로 실수를 다루려면 부동소수점
이라는 더 복잡한 인코딩이 필요합니다.
부동소수점은 일반적으로 실수를 다루는
합리적인 수단이지만 불편함 점도 있어
정수에는 더 간단한 인코딩을 사용할
가치가 있으며 즉 우리는 숫자를
저장하는 방법을 최소한 두가지 가집니다.
하나는 정수, 하나는 일반적인 실수를
위한 것으로 각 접근법은 서로 다른
하스켈 타입에 대응합니다.
더욱이 컴퓨터는 (+) 같은 연산을
동일 포멧의 숫자들에 대해서만
수행할 수 있습니다.
범우주적 Number 타입은 고사하고
정수와 실수를 썩는 (+) 도 쓸수 없는
것처럼 보입니다.
그런데 하스켈은 적어도 정수들 또는
실수들 사이에는 동일한 (+) 함수를
사용할 수 있습니다.
Prelude > 3 + 4
7
Prelude > 6.56 + 5.24
11.8
리스트와 튜플을 논할 때
함수가 다형성이라면 서로 다른
타입의 인자들을 받아들일 수 있는
걸 보았습니다. 어떤 사실을
고려해보면 ( + )의 타입 시그니처로
이런게 가능할 것 같습니다.
(+) :: -> a -> a -> a
(+)는 a라는 동일 타입의 두 인수를 취해
a 타입의 결과로 평가할 것 입니다.
하지만 이 해결책에는 문제가 있는데
앞서 봤듯 타입 변수 a는 모든 타입을
나타낼 수 있습니다.
(+)의 타입 시그니처가 이것이라면
두 Bool도 두 Char로 더할 수 있다는
건데 그다지 말이 되지 않습니다.
대신 (+)의 실제 시그니쳐는
한 언어 특성을 활용하는데 이 특성은
a가 숫자 타입인 한 어느 타입이든
될 수 있다고 의미상 제한을 거는 것을
가능케 합니다.
(+) :: (Num a) => a -> a -> a
Num은 타입클래스로, 숫자로 간주되는
모든 타입을 아우르는 타입들의 모임입니다.
시그니쳐의 (Num a) => 부분은 a를
숫자형으로, 좀 더 정확하게는
Num의 인스턴스들으로 제한합니다.
숫자 타입들
그러면 시그니쳐에서 a가 나타내는
Num의 인스턴스라는 건 실제로는
무슨 숫자 타입일까요? 가장 중요한
숫자 타입은 Int, Integer, Double입니다.
- Int 는 대부분 언어의 그 정수 타입에 대응합니다.
컴퓨터의 프로세서에 따라 고정된 최댓갑과
최소값을 가집니다.
(32 비트 기계에서는 -2147483648에서 2147483647까지) - Integer도 정수를 위해 쓰이지만 Int와 달리 효율성을
조금 희생해서 임의 크기의 값을 지원합니다. - Double은 배정밀도 부동소수점 타입으로,
대다수의 경우 실수를 위한 좋은 선택입니다.
(Float 이라고 Double의 단정밀도 친구가 있는데 정밀도의
추가 손실 때문에 대개는 Double에 밀립니다.)
이들 타입은 하스켈에서 기본적으로
사용할 수 있고 일괄 작업에서
쓰게 될 것들 입니다.
다형성 추측
아직 설명하지 않은게 하나 더 있는데
시작부에서 언급한 덧셈 예시를
시도해봤다면 이런 것이 완벽히
타당하단 걸 알고 있을 것 입니다.
Prelude> (-7) + 5.12
-1.88
여기서는 서로 다른 타입의 두 숫자를
더하는 것 같습니다. 정수와 정수가 아닌 것...
(+)의 타입 때문에 불가능 한 것인가?
이 물음에 답하려면 우리가 입력한
숫자들이 실제로 무슨 타입인지 봐야 합니다.
Prelude> :t (-7)
(-7) :: Num a => a
(-7)은 Int도 Integer도 아니며 이것은
다형성 상수로서 필요한 어느 숫자 타입으로든
변할 수 있으며 그 이유로는
다른 숫자를 살펴보면 명확해 집니다.
Prelude> :t 5.13
5.13 :: Fractional p => p
5.13도 다형성 상수인데 Num보다 제한적인
Functional 클래스에 속하는 것 입니다.
모든 Functional은 Num이지만 모든 Num이
Functional인 것은 아닙니다.
하스켈 프로그램은 (-7) + 5.12를 평가할 때,
이 숫자들의 실제 타입을 결정해야 합니다.
그 방법은 클래스 명세를 고려하여
타입 추론을 수행하는 것입니다.
(-7)은 어떤 Num도 될 수 있지만 5.13에는
추가적인 제한이 있어서 5.13의 타입인
(-7)이 무엇이 될 지를 결정할 것입니다.
둘의 타입이 어때야 하는지에 대한
다른 단서가 없기 때문에 5.12는
기본적인 Functional 타입인 Double로
간주될 것 입니다.
따라서 (-7)도 Double이 되고 덧셈을
진행하는 것이 가능하여 Double을
반환받게 됩니다.
이 과정을 더 와닿게 하는 빠른 검사가 있습니다.
소스 파일에서는 이렇게 정의합니다.
x = 2
그리고 이 파일을 GHCi로 불러와 x의
타입을 확인합니다. 그 다음 파일에
변수 y를 추가합니다.
x = 2
y = x + 3
파일을 다시 불러와 x와 y의 타입을
확인합니다. 마지막으로 y를 수정 후
x = 2
y = x + 3.1
두 변수의 타입이 어떤 값을 불러오는지
확인해 볼 수 있습니다.
단일형 문제
숫자 타입과 클래스의 정교함은
복합적으로 이어지기도 합니다.
가령, 흔한 나누기 연산자인 ( / )를
보면 그 타입 시그니처는 다음과 같습니다.
(/) :: (Fractional a) => a -> a -> a
a를 분수형으로 제한하는 것은
필수사항인데 두 정수의 나눗셈 결과는
대개 정수가 아니기 때문입니다.
그래도 여전히 이렇게 작성할 수 있습니다.
Prelude> 4 / 3
1.3333333333333333
리터럴 4와 3은 다형성 상수고 ( / )의
요구에 따라 Double 타입으로
간주되기 때문입니다.
하지만 한 숫자를 리스트의 길이로
나누는 것을 가정해 봅시다.
당연히 length 함수를 사용해야
할 것입니다.
Prelude> 4 / length [1,2,3]
하지만 이런 오류가 발생하는데
<interactive>:1:0:
No instance for (Fractional Int)
arising from a use of `/' at <interactive>:1:0-17
Possible fix: add an instance declaration for (Fractional Int)
In the expression: 4 / length [1, 2, 3]
In the definition of `it': it = 4 / length [1, 2, 3]
평소와 마찬가지로 이 문제는 length의
타입 시그니쳐를 보면 이해할 수 있습니다.
length :: [a] -> Int
length의 결과는 다형성 상수가 아니라
Int입니다. 그리고 Int는 Functional이
아니기 때문에 ( / )의 시그니쳐에
들어맞지 않습니다.
이 문제에서 탈출하는 수단을 제공하는
편리한 함수가 있습니다.
fromIntegral :: (Integral a, Num b) => a -> b
fromIntegral은 Integral 타입의 무언가를
인자로 취해서 다형성 상수로 만듭니다.
length와 결합하면 리스트의 길이를
( / )의 시그니쳐에 맞출 수 있습니다.
Prelude> 4 / fromIntegral (length [1,2,3])
1.3333333333333333
처음에는 이 표현식이 과도하게 복잡해
보이겠지만 이런 방법이 숫자를 조작할 때
표현식을 더 철처하게 만듭니다.
인자가 Int인 함수를 정의하면 그 인자는
절대 알아서 Integer나 Duble로
변환되지 않습니다.
fromIntegral 같은 함수로 프로그램에게
그 일을 명확히 지시해야 합니다.
이런 절제된 타입 체계의 결과로
하스켈에서는 숫자를 다루는 놀랍도록
다양한 클래스와 함수들이 있습니다.
숫자 너머의 클래스
타입클래스의 용도는 산술을 넘어
많은 것이 있습니다.
가령 (==)의 타입 시그니쳐는
다음과 같습니다.
(==) :: (Eq a) => a -> a -> Bool
(+) 나 ( / )처럼 (==)도 다형성 함수입니다.
(==)는 같은 타입의 두 값을 비교해
Bool 값을 반환하는데, 이 두 값은
반드시 Eq 클래스에 속해야 합니다.
Eq는 항등 비교가 가능한 값들의
타입들의 클래스이며 모든 기본적인
비함수형 타입들이 여기에 포함됩니다.
타입클래스는 타입 체계에 많은 강력함을
보태는 아주 범용적인 언어 특성입니다.
참고 자료