백엔드/Haskell

[Haskell] 하스켈 기초반 5강 - 타입의 기초2

Koras02 2023. 2. 5. 22:55

이번 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는 항등 비교가 가능한 값들의

타입들의 클래스이며 모든 기본적인

비함수형 타입들이 여기에 포함됩니다. 

 

타입클래스는 타입 체계에 많은 강력함을

보태는 아주 범용적인 언어 특성입니다.

 

참고 자료

 

6 타입의 기초2

원문: [http://en.wikibooks.org/wiki/Haskell/Type_basics_II](http://en.wikibooks.org/wiki/Haskell/Type…

wikidocs.net