이번 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는 항등 비교가 가능한 값들의
타입들의 클래스이며 모든 기본적인
비함수형 타입들이 여기에 포함됩니다.
타입클래스는 타입 체계에 많은 강력함을
보태는 아주 범용적인 언어 특성입니다.
참고 자료
'백엔드 > Haskell' 카테고리의 다른 글
[Haskell] 하스켈 기초반 6강 - 패턴 매칭과 if 표현식 및 let 바인딩 (0) | 2023.02.17 |
---|---|
[Haskell] 하스켈 기초반 4강 - 리스트와 튜플 (0) | 2023.01.29 |
[Haskell] 하스켈 기초반 3강 - 타입의 기초 (0) | 2023.01.12 |