계정: 로그인
AA 📝
10. 수치

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


Haskell은 Scheme [7]의 수치형에 기반하는 방대한 양의 수치형 타입들을 제공하며, 이 Scheme은 다시 Common Lisp [8]에 기반하고 있습니다. (그러나 이 언어들은 동적인 타입을 갖는 언어들입니다.) 표준 타입에는 고정정밀도 정수와 임의정밀도 정수, 각각의 정수 타입들로부터 만들어지는 비율 (유리수), 그리고 단정도 부동소수점 실수와 복소수, 배정도 부동소수점 실수와 복소수 등이 있습니다. 여기서는 수치형 타입 클래스 구조의 기본적인 특성에 대해서 간단히 짚어보기로 하며, 더 자세한 사항은 §6.4를 참조하시기 바랍니다.

10.1 수치형 계열 클래스 구조

수치형 타입 클래스들(Num 클래스와 그 하위에 있는 클래스들)은 많은 표준 Haskell 클래스들과 깊은 관련이 있습니다. NumEq의 서브클래스지만 Ord의 서브클래스는 아니라는 점에 주의하십시오. 순서를 나타내는 빈위어는 복소수에는 적용될 수 없기 때문입니다. 그러나 Num의 서브클래스인 RealOrd의 서브클래스이기도 합니다.

클래스 Num에는 모든 수치형 타입들에 대해 공통적으로 작동하는 기본적인 연산들이 들어있으며, 여기에는 덧셈, 뺄셈, 음수화, 곱셈, 절대값 등이 포함됩니다:

negate는 Haskell의 유일한 전위연산자인 minus로 적용되는 함수입니다; (-)는 뺄셈 함수로 쓰고 있기 때문에 이렇게 부르지 못하고 대신 negate라는 이름을 붙였습니다. 예를 들어, -x*ynegate (x*y)와 동일합니다. (전위 음수 부호의 구문상 우선 순위는 중위 뺄셈 기호와 똑같으며, 물론 이것은 곱셈 기호의 우선 순위보다 낮습니다.)

클래스 Num은 나누기 연산자를 제공하지 않습니다; Num의 서로 교차되지 않는 두 서브클래스에서 두 종류의 나누기 연산자를 각각 따로 제공합니다:

클래스 Integral은 몫과 나머지 연산을 제공합니다. Integral의 표준 인스턴스는 (‘bignums’라고도 알려져 있고, 바운드되지 않은, 혹은 수학적인 정수인) Integer와 (최소한 29비트의 부호있는 이진수와 같은 범위의, 바운드된 기계적 정수인) Int입니다. 어떤 Haskell 구현물은 이것 말고도 추가적으로 또다른 integral 타입을 제공할 수도 있습니다. IntegralNum으로부터 직접 상속된 서브클래스가 아니라 Real의 서브클래스라는 점에 주목하십시오; 이것은 가우스 정수를 제공하려는 시도가 없었음을 의미합니다.

다른 모든 수치 타입들은 클래스 Fractional에 속하며, 이 클래스는 평범한 나눗셈 연산자인 (/)를 제공합니다. 이것의 서브클래스인 Floating에는 삼각함수, 로그함수, 지수함수 등이 들어있습니다.

FractionalReal의 서브클래스인 RealFrac은 수치를 정부수와 소수부로 분리시키는 함수 properFraction을 제공하고, 서로 다른 규칙에 따라 정수값으로 변경시키는 일련의 함수들을 제공합니다:

FloatingRealFrac의 서브클래스인 RealFloat은 부동소수점 수의 구성 요소인 exponentsignificand에 효율적으로 접근하기 위한 몇가지 특수 함수들을 제공합니다. 표준 타입인 FloatDouble은 클래스 RealFloat 소속입니다.

10.2 복합 (Constructed) 수치형

표준 수치 타입들 중 Int, Integer, Float, Double은 원시 타입입니다. 나머지 수치형들은 구성자를 이용해서 이들로부터 만들어집니다.

(Complex 라이브러리에서 찾을 수 있는) ComplexFloating 클래스에서 RealFloat 타입으로부터 복소수 타입을 만드는 타입구성자입니다:

   1 data  (RealFloat a) => Complex a    =  !a :+ !a   deriving (Eq, Text)

기호 !6.3절에서 논의했던 strictness flag입니다. 인수 타입을 제한하고 있는 context인 RealFloat a에 주목하십시오. 이렇게, 표준 복소수 타입은 Complex FloatComplex Double입니다. data 선언으로부터 또 한 가지 알 수 있는 것은, 복소수는 x :+ y라고 쓴다는 점입니다: 인수들은 각각 실수부와 허수부의 데카르트 곱입니다. :+는 자료생성자기 때문에 이것을 패턴 매칭에 사용할 수 있습니다:

   1 conjugate             :: (RealFloat a) => Complex a -> Complex a
   2 conjugate (x :+ y)    =  x :+ (-y)

마찬가지로, (Rational 라이브러리에 있는) 타입구성자 Ratio클래스 RealFrac에서 Integral의 인스턴스로부터 유리수 타입을 만듭니다. (RationalRatio Integer의 타입동의어입니다.) 그러나, Ratio는 추상 타입구성자입니다. 유리수들은 두 개의 정수들로부터 비율을 만들어내는 데에 있어서 :+ 같은 자료구성자 대신 함수 ‘%’를 사용합니다. 패턴 매칭 대신 구성 요소를 추출하는 함수들이 제공됩니다:

왜 이렇게 다를까요? 데카르트 곱 형태의 복소수는 유일합니다 — :+와 관련하여 nontrivial identities가 없습니다. 반면에, 유리수는 유일하지 않으며, 추상자료형에 대한 구현에서 반드시 고려해야 하는 약분 형태가 존재합니다. 즉, 예를 들어 x:+y의 실수부는 언제나 x인 반면, numerator (x%y)가 꼭 x와 같다고 할 수는 없습니다.

10.3 수치 강제 변환과 오버로드된 상수들

표준 Prelude와 라이브러리에는 명시적인 강제 변환을 위한 몇가지 오버로드된 함수들이 있습니다:

   1 fromInteger     :: (Num a) => Integer -> a
   2 fromRational    :: (Fractional a) => Rational -> a
   3 toInteger       :: (Integral a) => a -> Integer
   4 toRational      :: (RealFrac a) => a -> Rational
   5 fromIntegral    :: (Integral a, Num b) => a -> b
   6 fromRealFrac    :: (RealFrac a, Fractional b) => a -> b
   7 
   8 fromIntegral    =  fromInteger . toInteger
   9 fromRealFrac    =  fromRational . toRational

이 중 두 가지는 오버로드된 수치 상수를 제공하기 위해 암묵적으로 사용되고 있습니다: (소수점이 없는) 정수 수치는 실제로는 그 수치의 Integer로서의 값에 fromInteger를 적용한 것과 동일합니다. 마찬가지로, (소수점이 있는) 부동소수점 수치는 그 수치의 Rational로서의 값에 fromRational를 적용한 것으로 간주합니다. 이렇게, 7의 타입은 (Num a) => a이고 7.3의 타입은 (Fractional a) => a입니다. 이것은 일반적인 수치 함수에서 수치 상수를 쓸 수 있음을 의미합니다. 예를 들어:

   1 halve      :: (Fractional a) => a -> a
   2 halve x    =  x * 0.5

수치를 오버로드하는 데에 있어서 이렇게 조금 간접적인 방법이 주는 추가적인 장점은 수치를 주어진 타입의 수로 해석하는 메서드가 Integral이나 Fractional 인스턴스 선언에 명시될 수 있다는 점입니다. (fromIntegerfromRational은 각각 이들 클래스의 연산자들이기 때문입니다.) 예를 들어, (RealFloat a) => Complex aNum 인스턴스는 다음과 같은 메서드를 포함합니다:

   1 fromInteger x    =  fromInteger x :+ 0

이것이 말해주는 것은 fromIntegerComplex 인스턴스는 실수부가 fromInteger의 적절한 RealFloat 인스턴스로부터 제공되는 복소수를 만들도록 정의되어 있다는 것입니다. 이런 식으로, (4원수 같은) 사용자 정의 수치 타입까지도 오버로드된 수치들을 사용할 수 있습니다.

또다른 예로서, 2장에 나왔던 inc의 첫 정의를 떠올려보시기 바랍니다:

   1 inc      :: Integer -> Integer
   2 inc n    =  n + 1

타입 지정자를 무시한다면, inc의 가장 일반적인 타입은 (Num a) => a->a입니다. 하지만 명시적인 타입지정자는 여전히 적절한데, 이것이 principal 타입보다 더 specific하기 때문입니다. (좀더 일반적인 타입 지정자는 static error를 낼 수 있습니다.) 타입지정자는 inc의 타입을 제한하는 효가가 있으며, 이 경우에는 inc (1::Float) 같은 것은 잘못된 타입이라고 말해줄 것입니다.

10.4 기본 (Default) 수치형

다음과 같은 함수 정의를 생각해봅시다:

   1 rms        :: (Floating a) => a -> a -> a
   2 rms x y    =  sqrt ((x ^ 2 + y ^ 2) * 0.5)

(서로 다른 타이핑을 갖는 세 가지 서로 다른 제곱 연산자 중 하나인 — Haskell Report 6.8.5절 참조) 제곱 함수 (^)의 타입은 (Num a, Integral b) => a -> b -> a이고, 2의 타입이 (Num a) => a이기 때문에 x^2의 타입은 (Num a, Integral b) => a입니다. 이것이 문제입니다. 즉, 타입 변수 b는 context 속에 들어있기 때문에 이 타입 변수 b와 연관된 오버로딩을 해소할 방법이 없지만, 그렇지 않았다면 이 타입 변수는 타입 표현식으로부터 사라져버렸을 것입니다. 결정적으로, 프로그래머는 x를 제곱해야 한다고 명시하긴 했지만, 이것을 2Int 값으로 제곱할지 Integer 값으로 제곱할지는 명시하지 않았습니다. 물론, 이 점을 보완할 수도 있습니다:

   1 rms x y    =  sqrt ((x ^ (2::Integer) + y ^ (2::Integer)) * 0.5)

하지만 분명히 이런 종류의 작업은 곧 피곤해집니다.

사실, 오버로딩에 있어서 이런 종류의 모호함은 수치에만 국한된 것이 아닙니다:

여기서 문자열을 어떤 타입으로 읽어야할까요? 이 문제는 제곱에 있어서의 모호함보다 훨씬 심각한데, 아까전에는 어떤 Integral 인스턴스라도 상관 없이 잘 동작했겠지만, 여기서는 모호함을 없애기 위해 어떤 Text 인스턴스를 사용하느냐에 따라 판이하게 다른 행동이 나타나기 때문입니다.

수치형의 경우와 일반적인 경우에 오버로딩의 모호함 문제가 달라지기 때문에, Haskell은 수치형에만 국한되는 해법을 제공하고 있습니다: 각각의 모듈들은 기본 선언(default declaration)을 가질 수 있으며, 이 기본 선언은 키워드 default와 그에 뒤따라 나오는, 괄호로 둘러싸이고 쉼표로 분리된 수치형 모노타입(변수가 없는 타입)의 리스트로 구성됩니다. (위의 b처럼) 모호한 타입 변수가 발견됐을 때, 만일 이 변수의 클래스들 중 최소한 하나가 수치형이고 클래스들 모두가 표준 클래스라면, 기본 리스트(default list)를 참고하여 이 리스트의 원소들의 타입 중 처음으로 타입 변수의 context를 만족시키는 타입이 사용됩니다. 예를 들어, 만일 기본 선언 default (Int, Float)가 유효하다면, 위에 나왔던 모호한 지수는 Int 타입으로 결정됩니다. (더 자세한 사항은 §4.3.4를 보시기 바랍니다.)

‘디폴트 디폴트’는 (Integer, Double)이지만 (Integer, Rational, Double)도 적절할 수 있습니다. 극히 조심스로운 프로그래머라면 아무런 디폴트도 제공하지 않는 default ()를 선호할지도 모릅니다.


이전 다음 위로
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.