미리 정의된 타입과 클래스

미리 정의된 타입과 클래스 (Predefined Types and Classes)

Haskell 프로그램은 어떤 import도 하지 않아도 기본으로 쓰는 타입·클래스·함수가 있어요. 바로 Prelude가 정의해 두는 것들이에요. 이 장은 그 Prelude 안에 있는 타입과 클래스가 무엇인지, 각각 어떤 규약(law)을 지켜야 하는지를 다뤄요. 숫자 타입과 연산, 모나드, 펑터 같은 Haskell의 "문법 그 자체가 아니다"고 해도 기본 배경이 되는 부품들이 여기에 있어요.

출처: Haskell 2010 언어 리포트

본문

Haskell Prelude는 모든 Haskell 프로그램에 암묵적으로 import되는 미리 정의된 클래스·타입·함수를 포함해요. 이 장에서는 Prelude에 있는 타입과 클래스를 설명해요. 대부분의 함수는 9장에 주어진 정의로 쉽게 이해할 수 있어서 여기서 자세히 설명하지 않아요. 배열·복소수·유리수 같은 다른 미리 정의된 타입은 Part II에 정의돼 있어요.

6.1 표준 Haskell 타입 (Standard Haskell Types)

이 타입들은 Haskell Prelude가 정의해요. 숫자 타입은 6.4절에서 설명해요. 적절할 때 타입의 Haskell 정의가 주어져요. 일부 정의는 구문상 완전히 유효하지 않을 수 있지만 기저 타입의 의미를 충실히 전달해요.

6.1.1 부울 (Booleans)

data Bool = False | True deriving
    (Read, Show, Eq, Ord, Enum, Bounded)

부울 타입 Bool은 열거형(enumeration)이에요. 기본 부울 함수는 &&(and), ||(or), not이에요. 이름 otherwise는 가드 식을 더 읽기 쉽게 하려고 True로 정의돼 있어요.

6.1.2 문자와 문자열 (Characters and Strings)

문자 타입 Char는 유니코드 문자를 나타내는 값들을 가진 열거형이에요 [2]. 문자에 대한 어휘 문법은 2.6절에 정의돼 있어요. 문자 리터럴은 데이터타입 Char의 무항(nullary) 생성자예요.

타입 Char는 클래스 Read, Show, Eq, Ord, Enum, Bounded의 인스턴스예요. 클래스 Enum의 표준 함수인 toEnumfromEnum은 문자를 Int 타입으로/로부터 매핑해요.

ASCII 제어 문자는 문자 리터럴에서 여러 표현을 가진다는 점에 주의해요: 숫자 이스케이프, ASCII 기억용(mnemonic) 이스케이프, \^X 표기. 게다가 다음 동등성이 있어요: \a\BEL, \b\BS, \f\FF, \r\CR, \t\HT, \v\VT, \n\LF.

문자열은 문자의 리스트예요:

type String = [Char]

문자열은 2.6절에서 설명한 어휘 문법으로 축약될 수 있어요. 예를 들어 "A string"은:

['A',' ','s','t','r','i','n','g']

을 축약해요.

6.1.3 리스트 (Lists)

data [a] = [] | a : [a] deriving (Eq, Ord)

리스트는 두 생성자의 대수적 데이터타입이지만, 3.7절에서 설명한 대로 특별한 문법을 가져요. 첫 생성자는 '[]'("nil")이라 쓰는 널 리스트이고, 두 번째는 ':'("cons")예요. 모듈 PreludeList(9.1절)는 많은 표준 리스트 함수를 정의해요. 특별한 종류의 리스트를 위한 두 편리한 문법인 산술 수열과 리스트 컴프리헨션은 각각 3.10절과 3.11절에서 설명해요. 리스트는 클래스 Read, Show, Eq, Ord, Monad, Functor, MonadPlus의 인스턴스예요.

6.1.4 튜플 (Tuples)

튜플은 3.8절에 정의된 대로 특별한 문법을 가진 대수적 데이터타입이에요. 각 튜플 타입은 단일 생성자를 가져요. 모든 튜플은 Eq, Ord, Bounded, Read, Show의 인스턴스예요(물론 그 모든 성분 타입이 그럴 때).

튜플 크기에 상한은 없지만, 어떤 Haskell 구현은 튜플 크기를 제한하고 더 큰 튜플과 연관된 인스턴스를 제한할 수 있어요. 그러나 모든 Haskell 구현은 크기 15까지의 튜플과 Eq, Ord, Bounded, Read, Show의 인스턴스를 지원해야 해요. Prelude와 라이브러리는 zip 같은 튜플 함수를 크기 7까지의 튜플에 대해 정의해요.

튜플의 생성자는 콤마를 둘러싼 식을 생략함으로써 써요. 그래서 (x,y)(,) x y는 같은 값을 만들어요. 튜플 타입 생성자도 마찬가지예요. (Int,Bool,Int)(,,) Int Bool Int는 같은 타입을 나타내요.

페어(2-튜플)에 대해 다음 함수가 정의돼 있어요: fst, snd, curry, uncurry. 더 큰 튜플에는 비슷한 함수가 미리 정의돼 있지 않아요.

6.1.5 유닛 데이터타입 (The Unit Datatype)

data () = () deriving (Eq, Ord, Bounded, Enum, Read, Show)

유닛 데이터타입 ()은 비-⊥ 멤버가 하나 있는데, 바로 무항 생성자 ()이에요. 3.9절도 봐요.

6.1.6 함수 타입 (Function Types)

함수는 추상 타입이에요. 함수 값을 직접 만드는 생성자는 없어요. Prelude에는 다음 단순 함수들이 있어요: id, const, (.), flip, ($), until.

6.1.7 IO와 IOError 타입 (The IO and IOError Types)

IO 타입은 외부 세계와 상호작용하는 연산(액션)을 위한 태그 역할을 해요. IO 타입은 추상적이에요. 사용자에게 보이는 생성자가 없어요. IOMonadFunctor 클래스의 인스턴스예요. 7장이 I/O 연산을 설명해요.

IOError는 I/O 연산이 일으키는 오류를 나타내는 추상 타입이에요. ShowEq의 인스턴스예요. 이 타입의 값은 다양한 I/O 함수로 만들어지고, 이 리포트에서 더 자세히 제시되지는 않아요.

Prelude에는 몇 가지 I/O 함수(9.3절에 정의)가 있고, Part II에는 훨씬 더 많아요.

6.1.8 다른 타입 (Other Types)

data Maybe a    = Nothing | Just a   deriving (Eq, Ord, Read, Show)
data Either a b = Left a | Right b   deriving (Eq, Ord, Read, Show)
data Ordering   = LT | EQ | GT       deriving
    (Eq, Ord, Bounded, Enum, Read, Show)

Maybe 타입은 클래스 Functor, Monad, MonadPlus의 인스턴스예요. Ordering 타입은 클래스 Ordcompare가 써요. 함수 maybeeither는 Prelude에 있어요.

6.2 엄격 평가 (Strict Evaluation)

Haskell의 함수 적용은 비엄격(non-strict)이에요. 즉 함수 인자는 요구될 때에만 평가돼요. 때로는 seq 함수를 써서 값의 평가를 강제하는 것이 바람직해요:

seq :: a -> b -> b

함수 seq는 방정식으로 정의돼요:

seq ⊥ b = ⊥
seq a b = b, if a ≠ ⊥

seq는 보통 불필요한 느긋함(laziness)을 피해 성능을 개선하기 위해 도입돼요. 엄격 데이터타입(4.2.1절)은 $! 연산자로 정의돼요. 그러나 seq의 제공은 중요한 의미론적 결과를 가져요. 모든 타입에서 사용 가능하기 때문이에요. 결과적으로 \x -> ⊥과 같지 않아요. seq로 그것들을 구별할 수 있기 때문이에요. 같은 이유로 seq의 존재는 Haskell의 매개변수성(parametricity) 성질을 약화시켜요.

연산자 $!는 엄격(호출-값, call-by-value) 적용이고 seq로 정의돼요. Prelude는 비엄격 적용을 수행하는 $ 연산자도 정의해요.

infixr 0 $, $!
($), ($!) :: (a -> b) -> a -> b
f $ x   =  f x
f $! x  =  x 'seq' f x

비엄격 적용 연산자 $는 중복돼 보일 수 있어요. 일반 적용 (f x)(f $ x)와 같기 때문이에요. 그러나 $는 낮고 우결합의 바인딩 우선순위를 가져서, 때로 괄호를 생략하게 해 줘요. 예:

f $ g $ h x = f (g (h x))

또한 map ($ 0) xszipWith ($) fs xs 같은 고차 상황에서도 유용해요.

6.3 표준 Haskell 클래스 (Standard Haskell Classes)

그림 6.1은 Prelude에 정의된 Haskell 클래스의 계층과, 이 클래스들의 인스턴스인 Prelude 타입을 보여줘요.

그림 6.1: 표준 Haskell 클래스

표준 클래스의 많은 메서드에 대해 기본 클래스 메서드 선언(4.3절)이 제공돼요. 9장의 각 클래스 선언과 함께 있는 주석이, 기본 선언과 함께 모든 클래스 메서드에 대한 합리적인 정의를 제공하는 가장 작은 메서드 정의 집합을 지정해요. 그런 주석이 없으면 모든 클래스 메서드를 주어야 인스턴스를 완전히 지정할 수 있어요.

6.3.1 Eq 클래스 (The Eq Class)

class Eq a where
    (==), (/=) :: a -> a -> Bool
    x /= y     = not (x == y)
    x == y     = not (x /= y)

Eq 클래스는 동등(==)과 비동등(/=) 메서드를 제공해요. 함수와 IO를 빼고 모든 기본 데이터타입이 이 클래스의 인스턴스예요. Eq의 인스턴스는 그 구성 요소가 Eq의 인스턴스인 어떤 사용자 정의 데이터타입에 대해서도 파생될 수 있어요.

이 선언은 /=== 둘 다에 기본 메서드 선언을 주는데, 각각이 다른 것으로 정의돼요. Eq에 대한 인스턴스 선언이 ==/=도 정의하지 않으면 둘 다 무한 루프를 돌아요. 하나가 정의되면 다른 하나의 기본 메서드가 정의된 것을 사용해요. 둘 다 정의되면 어떤 기본 메서드도 쓰이지 않아요.

6.3.2 Ord 클래스 (The Ord Class)

class (Eq a) => Ord a where
    compare              :: a -> a -> Ordering
    (<), (<=), (>=), (>) :: a -> a -> Bool
    max, min             :: a -> a -> a
    compare x y | x == y = EQ
                | x <= y = LT
                | otherwise = GT
    x <= y               = compare x y /= GT
    x <  y               = compare x y == LT
    x >= y               = compare x y /= LT
    x >  y               = compare x y == GT
        -- Note that (min x y, max x y) = (x,y) or (y,x)
    max x y | x <= y     = y
            | otherwise = x
    min x y | x <= y     = x
            | otherwise = y

Ord 클래스는 완전 순서를 가진 데이터타입(total ordered datatypes)에 쓰여요. 함수·IO·IOError를 빼고 모든 기본 데이터타입이 이 클래스의 인스턴스예요. Ord의 인스턴스는 구성 타입이 Ord에 있는 어떤 사용자 정의 데이터타입에 대해서도 파생될 수 있어요. data 선언에서 생성자의 선언된 순서가 파생된 Ord 인스턴스의 순서를 결정해요. Ordering 데이터타입은 한 번의 비교로 두 객체의 정확한 순서를 결정할 수 있게 해 줘요.

기본 선언 덕분에 사용자는 타입 특정 compare 함수로, 또는 타입 특정 ==<= 함수로 Ord 인스턴스를 만들 수 있어요.

6.3.3 Read와 Show 클래스 (The Read and Show Classes)

type ReadS a = String -> [(a,String)]
type ShowS   = String -> String

class Read a where
    readsPrec :: Int -> ReadS a
    readList  :: ReadS [a]
    -- ... default decl for readList given in Prelude

class Show a where
    showsPrec :: Int -> a -> ShowS
    show      :: a -> String
    showList  :: [a] -> ShowS

    showsPrec _ x s = show x ++ s
    show x          = showsPrec 0 x ""

    -- ... default decl for showList given in Prelude

ReadShow 클래스는 값을 문자열로/로부터 변환하는 데 쓰여요. showsPrecreadsPrecInt 인자는 둘러싼 문맥의 연산자 우선순위를 줘요(11.4절).

showsPrecshowList는 String-to-String 함수를 돌려줘서, 함수 합성을 써서 결과를 상수 시간에 이어붙일 수 있게 해 줘요. 우선순위 문맥 0을 쓰고 일반 String을 돌려주는 전문화된 변형 show도 제공돼요. showList 메서드는 프로그래머가 값 리스트를 보여주는 전문화된 방식을 줄 수 있게 하려고 제공돼요. 이것은 Char 타입에서 특히 유용한데, String 타입의 값은 대괄호 사이가 아니라 쌍따옴표 사이에 보여줘야 하기 때문이에요.

ReadShow의 파생 인스턴스는 생성자가 선언된 스타일을 반복해요. 중위 생성자와 필드 이름이 입력·출력에 쓰여요. showsPrec가 만든 문자열은 대개 readsPrec가 읽을 수 있어요.

함수 타입과 IO 타입을 빼고 모든 Prelude 타입이 ShowRead의 인스턴스예요. (원하면 프로그래머가 인스턴스 선언을 제공해 함수·IO 타입을 쉽게 (공허한, vacuous) Show 인스턴스로 만들 수 있어요.)

편의를 위해 Prelude는 다음 보조 함수를 제공해요:

reads :: (Read a) => ReadS a
reads = readsPrec 0
shows :: (Show a) => a -> ShowS
shows = showsPrec 0
read :: (Read a) => String -> a
read s = case [x | (x,t) <- reads s, ("","") <- lex t] of
    [x] -> x
    []  -> error "PreludeText.read: no parse"
    _   -> error "PreludeText.read: ambiguous parse"

showsreads는 기본 우선순위 0을 써요. read 함수는 문자열에서 입력을 읽는데, 입력 과정이 문자열을 완전히 소비해야 해요.

read가 쓰는 함수 lex :: ReadS String도 Prelude의 일부예요. 입력에서 단일 lexeme을 읽고, 초기 공백을 버리고, lexeme을 이루는 문자들을 돌려줘요. 입력 문자열이 공백만 포함하면 lex는 빈 문자열로 이뤄진 단일 성공 "lexeme"을 돌려줘요. (그래서 lex "" = [("","")]이에요.) 입력 문자열 시작에 합법적 lexeme이 없으면 lex는 실패해요(즉 []를 돌려줘요).

6.3.4 Enum 클래스 (The Enum Class)

class Enum a where
    succ, pred       :: a -> a
    toEnum           :: Int -> a
    fromEnum         :: a -> Int
    enumFrom         :: a -> [a]             -- [n..]
    enumFromThen     :: a -> a -> [a]        -- [n,n'..]
    enumFromTo       :: a -> a -> [a]        -- [n..m]
    enumFromThenTo   :: a -> a -> a -> [a]   -- [n,n'..m]
    -- Default declarations given in Prelude

클래스 Enum은 순차적으로 순서가 있는 타입(sequentially ordered types)에 대한 연산을 정의해요. 함수 succpred는 각각 값의 후계자(successor)와 선임자(predecessor)를 돌려줘요. 함수 fromEnumtoEnumEnum의 타입의 값을 Int로/로부터 매핑해요. enumFrom... 메서드들은 산술 수열(3.10절)을 번역할 때 쓰여요.

Enum의 인스턴스는 어떤 열거형 타입(생성자에 필드가 없는 타입)에 대해서도 파생될 수 있어요. 11장을 봐요.

BoundedEnum 둘 다의 인스턴스인 어떤 타입에 대해 다음이 성립해야 해요:

  • 호출 succ maxBoundpred minBound는 실행 시 오류를 내야 해요.
  • 결과 값이 결과 타입에 표현 가능하지 않으면 fromEnumtoEnum이 실행 시 오류를 줘야 해요. 예를 들어 toEnum 7 :: Bool은 오류예요.
  • enumFromenumFromThen은 암묵적 경계로 정의되어야 해요. 즉:
    enumFrom x      = enumFromTo x maxBound
    enumFromThen x y = enumFromThenTo x y bound
                      where
                      bound | fromEnum y >= fromEnum x = maxBound
                            | otherwise                = minBound
    

다음 Prelude 타입이 Enum의 인스턴스예요:

  • 열거형 타입: (), Bool, Ordering. 이 인스턴스들의 의미는 11장에서 주어져요. 예를 들어 [LT ..]는 리스트 [LT,EQ,GT]예요.
  • Char: 인스턴스는 CharInt 사이를 변환하는 원시 함수에 기반해 9장에서 주어져요. 예를 들어 enumFromTo 'a' 'z'는 알파벳 순서의 소문자 리스트를 나타내요.
  • 숫자 타입: Int, Integer, Float, Double. 이 인스턴스들의 의미는 다음에 주어져요.

네 숫자 타입 모두에서 succ는 1을 더하고, pred는 1을 뺀다. 변환 fromEnumtoEnum은 타입과 Int 사이를 변환해요. FloatDouble의 경우 소수점 뒤 자릿수가 잃어질 수 있어요. Int에 맞기엔 너무 큰 값에 적용했을 때 fromEnum이 무엇을 돌려줄지는 구현 의존적이에요.

IntInteger 타입에 대해 열거 함수들은 다음 의미를 가져요:

  • 수열 enumFrom e1은 리스트 [e1,e1 + 1,e1 + 2,…]이에요.
  • 수열 enumFromThen e1 e2는 리스트 [e1,e1 + i,e1 + 2i,…]이에요. 여기서 증분(increment) ie2 − e1이에요. 증분은 0이거나 음수일 수 있어요. 증분이 0이면 모든 리스트 원소가 같아요.
  • 수열 enumFromTo e1 e3는 리스트 [e1,e1 + 1,e1 + 2,…e3]이에요. e1 > e3이면 리스트는 비어요.
  • 수열 enumFromThenTo e1 e2 e3은 리스트 [e1,e1 + i,e1 + 2i,…e3]이에요. 여기서 증분 ie2 − e1이에요. 증분이 양수나 0이면, 다음 원소가 e3보다 커지려 할 때 리스트가 끝나요. e1 > e3이면 리스트는 비어요. 증분이 음수면, 다음 원소가 e3보다 작아지려 할 때 리스트가 끝나요. e1 < e3이면 리스트는 비어요.

FloatDouble에 대해 enumFrom 족의 의미는 위 Int 규칙으로 주어지는데, 다음 한 가지가 달라요: 양수 증분 i에 대해 원소가 e3 + i∕2보다 커지거나, 음수 i에 대해 e3 + i∕2보다 작아지면 리스트가 끝나요.

네 Prelude 숫자 타입 모두에서 enumFrom 족의 모든 함수는 모든 인자에 대해 엄격해요.

6.3.5 Functor 클래스 (The Functor Class)

class Functor f where
    fmap :: (a -> b) -> f a -> f b

Functor 클래스는 매핑될 수 있는(mapped over) 타입에 쓰여요. 리스트·IO·Maybe가 이 클래스에 있어요.

Functor의 인스턴스는 다음 법칙을 만족해야 해요:

fmap id            =  id
fmap (f . g)       =  fmap f . fmap g

Prelude에 정의된 모든 Functor 인스턴스는 이 법칙을 만족해요.

6.3.6 Monad 클래스 (The Monad Class)

class Monad m where
    (>>=)  :: m a -> (a -> m b) -> m b
    (>>)   :: m a -> m b -> m b
    return :: a -> m a
    fail   :: String -> m a

    m >> k   =  m >>= \_ -> k
    fail s   =  error s

Monad 클래스는 모나드에 대한 기본 연산을 정의해요. 모나드에 대한 더 많은 정보는 7장을 봐요. "do" 식은 모나딕 식을 쓰는 편리한 문법을 제공해요(3.14절). fail 메서드는 do 식에서 패턴 매칭 실패 시 호출돼요.

Prelude에서 리스트·Maybe·IO는 모두 Monad의 인스턴스예요. 리스트의 fail 메서드는 빈 리스트 []를 돌려주고, MaybeNothing을 돌려주며, IO는 IO 모나드에서 사용자 예외를 일으켜요(7.3절).

Monad의 인스턴스는 다음 법칙을 만족해야 해요:

return a >>= k      =  k a
m >>= return        =  m
m >>= (\x -> k x >>= h)  =  (m >>= k) >>= h

MonadFunctor 둘 다의 인스턴스는 추가로 다음 법칙을 만족해야 해요:

fmap f xs  =  xs >>= return . f

Prelude에 정의된 모든 Monad 인스턴스는 이 법칙을 만족해요.

Prelude는 다음 보조 함수를 제공해요:

sequence  :: Monad m => [m a] -> m [a]
sequence_ :: Monad m => [m a] -> m ()
mapM      :: Monad m => (a -> m b) -> [a] -> m [b]
mapM_     :: Monad m => (a -> m b) -> [a] -> m ()
(=<<)     :: Monad m => (a -> m b) -> m a -> m b

6.3.7 Bounded 클래스 (The Bounded Class)

class Bounded a where
    minBound, maxBound :: a

Bounded 클래스는 타입의 상한·하한에 이름을 붙이는 데 쓰여요. OrdBounded의 슈퍼클래스가 아니에요. 완전히 순서가 없는 타입도 상한·하한을 가질 수 있기 때문이에요. 타입 Int, Char, Bool, (), Ordering, 그리고 모든 튜플은 Bounded의 인스턴스예요. Bounded 클래스는 어떤 열거형 타입에 대해서도 파생될 수 있어요. minBounddata 선언에 나열된 첫 생성자이고 maxBound는 마지막이에요. Bounded는 구성 타입이 Bounded에 있는 단일 생성자 데이터타입에 대해서도 파생될 수 있어요.

6.4 숫자 (Numbers)

Haskell은 여러 종류의 숫자를 제공해요. 숫자 타입과 그 연산은 Common Lisp와 Scheme의 강한 영향을 받았어요. 숫자 함수 이름과 연산자는 대개 오버로드돼 있고, 그림 6.1에 포함 관계(inclusion relation)로 보이는 여러 타입 클래스를 써요. 숫자 타입의 클래스 NumEq의 서브클래스인데, 모든 숫자는 동등 비교가 가능하기 때문이에요. 그 서브클래스 RealOrd의 서브클래스이기도 한데, 다른 비교 연산이 복소수(Complex 라이브러리에 정의)를 뺀 모든 것에 적용되기 때문이에요.

클래스 Integral은 제한·무제한 범위의 정수를 모두 포함해요. 클래스 Fractional은 모든 비-정수 타입을 포함하고, 클래스 Floating은 실수·복소수를 모두 포함하는 모든 부동소수점 타입을 포함해요.

Prelude는 가장 기본적인 숫자 타입만 정의해요: 고정 크기 정수(Int), 임의 정밀도 정수(Integer), 단정밀도 부동소수점(Float), 배정밀도 부동소수점(Double). 유리수·복소수 같은 다른 숫자 타입은 라이브러리에 정의돼 있어요. 특히 타입 RationalRatio 라이브러리에 정의된 대로 두 Integer 값의 비율이에요.

Haskell Prelude가 정의한 기본 부동소수점 연산은 현재의 언어 독립 산술(LIA) 표준을 따르지 않아요. 이 표준들은 숫자 구조에 훨씬 더 많은 복잡성을 요구해서, 그래서 라이브러리로 미뤄졌어요. IEEE 부동소수점 표준의 일부(전부는 아닌) 측면이 Prelude 클래스 RealFloat에서 고려됐어요.

표준 숫자 타입은 표 6.1에 나열돼 있어요. 유한 정밀도 정수 타입 Int는 적어도 범위 [−2^29, 2^29 − 1]을 다뤄요. IntBounded 클래스의 인스턴스이므로, maxBoundminBound로 구현이 정의한 정확한 Int 범위를 결정할 수 있어요. Float는 구현 정의예요. 이 타입이 적어도 IEEE 단정밀도 타입만큼 범위·정밀도가 같으면 바람직해요. 마찬가지로 Double은 IEEE 배정밀도를 다뤄야 해요. 고정 정밀도 숫자 타입의 예외 조건(오버플로·언더플로 같은)의 결과는 정의되지 않아요. 구현은 error(의미론적으로 ⊥), 잘린 값, 또는 무한대·무한(indefinite) 같은 특별 값을 선택할 수 있어요.

표 6.1: 표준 숫자 타입

Type                           Class      Description
Integer                        Integral   Arbitrary-precision integers
Int                            Integral   Fixed-precision integers
(Integral a) => Ratio a        RealFrac   Rational numbers
Float                          RealFloat  Real floating-point, single precision
Double                         RealFloat  Real floating-point, double precision
(RealFloat a) => Complex a     Floating   Complex floating-point

Prelude에 정의된 표준 숫자 클래스와 다른 숫자 함수는 그림 6.2–6.3에 있어요. 그림 6.1은 클래스 의존성과 숫자 클래스의 인스턴스인 내장 타입을 보여줘요.

그림 6.2: 표준 숫자 클래스와 관련 연산, 1부

class (Eq a, Show a) => Num a where
    (+), (-), (*)       :: a -> a -> a
    negate              :: a -> a
    abs, signum         :: a -> a
    fromInteger         :: Integer -> a

class (Num a, Ord a) => Real a where
    toRational          :: a -> Rational

class (Real a, Enum a) => Integral a where
    quot, rem, div, mod :: a -> a -> a
    quotRem, divMod     :: a -> a -> (a,a)
    toInteger           :: a -> Integer

class (Num a) => Fractional a where
    (/)                 :: a -> a -> a
    recip               :: a -> a
    fromRational        :: Rational -> a

class (Fractional a) => Floating a where
    pi                  :: a
    exp, log, sqrt      :: a -> a
    (**), logBase       :: a -> a -> a
    sin, cos, tan       :: a -> a
    asin, acos, atan    :: a -> a
    sinh, cosh, tanh    :: a -> a
    asinh, acosh, atanh :: a -> a

그림 6.3: 표준 숫자 클래스와 관련 연산, 2부

class (Real a, Fractional a) => RealFrac a where
    properFraction      :: (Integral b) => a -> (b,a)
    truncate, round     :: (Integral b) => a -> b
    ceiling, floor      :: (Integral b) => a -> b

class (RealFrac a, Floating a) => RealFloat a where
    floatRadix          :: a -> Integer
    floatDigits         :: a -> Int
    floatRange          :: a -> (Int,Int)
    decodeFloat         :: a -> (Integer,Int)
    encodeFloat         :: Integer -> Int -> a
    exponent            :: a -> Int
    significand         :: a -> a
    scaleFloat          :: Int -> a -> a
    isNaN, isInfinite, isDenormalized, isNegativeZero, isIEEE
                        :: a -> Bool
    atan2               :: a -> a -> a

gcd, lcm               :: (Integral a) => a -> a-> a
(^)                    :: (Num a, Integral b) => a -> b -> a
(^^)                   :: (Fractional a, Integral b) => a -> b -> a
fromIntegral           :: (Integral a, Num b) => a -> b
realToFrac             :: (Real a, Fractional b) => a -> b

6.4.1 숫자 리터럴 (Numeric Literals)

숫자 리터럴의 문법은 2.5절에 있어요. 정수 리터럴은 fromInteger 함수를 적절한 Integer 타입 값에 적용한 것이에요. 마찬가지로 부동소수점 리터럴은 fromRationalRational(즉 Ratio Integer) 타입 값에 적용한 것이에요. 타이핑:

fromInteger :: (Num a) => Integer -> a
fromRational :: (Fractional a) => Rational -> a

이 주어지면, 정수·부동소수점 리터럴은 각각 타이핑 (Num a) => a(Fractional a) => a를 가져요.

숫자 리터럴은 이렇게 간접적으로 정의돼서, 어떤 적절한 숫자 타입의 값으로 해석될 수 있어요. 오버로딩 모호성에 대한 논의는 4.3.4절을 봐요.

6.4.2 산술·정수론 연산 (Arithmetic and Number-Theoretic Operations)

중위 클래스 메서드 (+), (*), (-)와 단항 함수 negate(전위 마이너스 부호로도 쓸 수 있음, 3.4절)는 모든 숫자에 적용돼요. 클래스 메서드 quot, rem, div, mod는 정수에만 적용되는 반면, 클래스 메서드 (/)는 분수에만 적용돼요. quot, rem, div, mod 클래스 메서드는 y가 0이 아닐 때 다음 법칙을 만족해요:

(x 'quot' y)*y + (x 'rem' y) == x
(x 'div'  y)*y + (x 'mod'  y) == x

'quot'는 0을 향해 잘리는 정수 나눗셈이고, 'div'의 결과는 음의 무한대를 향해 잘려요. quotRem 클래스 메서드는 피제수(dividend)와 제수(divisor)를 인자로 받아 (몫, 나머지) 쌍을 돌려줘요. divMod도 비슷하게 정의돼요:

quotRem x y = (x 'quot' y, x 'rem' y)
divMod  x y = (x 'div'  y, x 'mod' y)

정수에는 evenodd 술어도 쓸 수 있어요:

even x = x 'rem' 2 == 0
odd  = not . even

마지막으로 최대공약수(GCD)·최소공배수(LCM) 함수가 있어요. gcd x yxy 둘 다를 나누는 가장 큰 (양의) 정수예요. 예를 들어 gcd (-3) 6 = 3, gcd (-3) (-6) = 3, gcd 0 4 = 4. gcd 0 0은 실행 시 오류를 일으켜요. lcm x yxy 둘 다가 나누는 가장 작은 양의 정수예요.

6.4.3 지수·로그 (Exponentiation and Logarithms)

단일 인자 지수 함수 exp와 로그 함수 log는 부동소수점 수에 작용하고 밑 e를 써요. logBase a x는 밑 ax 로그를 돌려줘요. sqrt는 부동소수점 수의 주 제곱근(principal square root)을 돌려줘요. 두 인자 지수 연산이 세 가지 있어요: (^)는 어떤 숫자를 음이 아닌 정수 거듭제곱으로, (^^)는 분수 숫자를 어떤 정수 거듭제곱으로, (**)는 두 부동소수점 인자를 받아요. 어떤 x에 대해(0 포함) x^0이나 x^^0의 값은 1이에요. 0**yy가 1이면 1, 그 외엔 0이에요.

6.4.4 크기와 부호 (Magnitude and Sign)

숫자는 크기(magnitude)와 부호(sign)를 가져요. 함수 abssignum은 어떤 숫자에 적용되고 법칙을 만족해요:

abs x * signum x == x

6.4.5 반올림 (Rounding)

properFraction, truncate, round, ceiling, floor는 정수 타입에 반환하는 반올림 함수예요.

  • properFraction x는 (정수부분, 소수부분) 쌍을 돌려줘요. 정수 부분은 truncate와 같고, 소수 부분은 x에서 정수 부분을 뺀 것과 같아요(부호 포함). 즉 properFraction (-1.2) = (-1, -0.2)이에요.
  • truncate x는 0을 향해 반올림해요(소수 부분을 버림).
  • round x는 가장 가까운 정수로 반올림해요. x가 경계(예: 0.5)에 있으면 짝수(even) 정수를 택해요.
  • ceiling xx 이상의 가장 작은 정수로 반올림해요.
  • floor xx 이하의 가장 큰 정수로 반올림해요.

각 반올림 함수는 RealFrac 클래스의 일부예요. roundfloor의 구체적 동작과 모호한 경우는 6.4.5절과 관련 클래스 정의를 참고해요.

6.4.6 부동소수점·배정밀도 (Floating-Point and Double Precision)

부동소수점 수는 밑(base)과 지수(exponent)와 가수(significand)로 분해될 수 있어요. 클래스 RealFloat의 메서드가 이것을 다뤄요:

  • floatRadix xx의 표현에 쓰이는 밑을 돌려줘요.
  • floatDigits xx의 부호 없는 가수의 자릿수(숫자 수)를 돌려줘요.
  • floatRange(floatRadix x)의 지수에 허용되는 (최소, 최대) 범위를 돌려줘요.
  • decodeFloat xx를 (가수, 지수) 쌍으로 분해해요. 가수는 정수이고, x = 가수 × 밑^지수 관계가 성립해요.
  • encodeFloat m ndecodeFloat의 역연산이에요: mn에서 m × 밑^n 값을 만들어요.
  • exponent xx의 지수를 돌려줘요.
  • significand xx의 가수를 돌려줘요.
  • scaleFloat n xx의 지수를 n만큼 바꿔요.
  • isNaN, isInfinite, isDenormalized, isNegativeZero, isIEEE는 부동소수점 값의 특수 상태를 검사하는 술어예요.
  • atan2 y x는 점 (x,y)의 극각(polar angle)을 돌려줘요.

6.4.7 기타 숫자 연산 (Other Numeric Operations)

  • gcdlcm은 정수에 대한 최대공약수·최소공배수예요.
  • (^)는 어떤 숫자를 음이 아닌 정수 거듭제곱으로 올려요. x^0 = 1이에요(0 포함). (^^)는 분수 숫자를 어떤 정수 거듭제곱으로 올려요. x^^0 = 1이에요(0 포함). (**)는 두 부동소수점 인자를 받아요.
  • fromIntegral은 정수를 Num 타입으로 변환해요. realToFracReal 타입을 Fractional 타입으로 변환해요.

Haskell Prelude의 표준 부동소수점 연산은 언어 독립 산술(LIA) 표준과 IEEE 표준의 일부 측면을 완전히 따르지 않아요. RealFloat 클래스는 IEEE 부동소수점의 여러 특수 값을 검사하는 함수를 제공해요.

더 알아보기 (Learn more)