6장 사전 정의된 타입과 클래스
6장 사전 정의된 타입과 클래스 (Predefined Types and Classes)
출처: https://www.haskell.org/onlinereport/haskell2010/haskellch6.html
본문
Haskell Prelude에는 미리 정의된 클래스와 타입, 함수가 담겨 있어요. 이 Prelude는 모든 Haskell 프로그램에 암묵적으로 임포트되죠. 이번 장에서는 바로 그 Prelude에 들어 있는 타입과 클래스들을 다뤄볼게요. 대부분의 함수는 여기서 길게 설명하지 않아요 — 9장의 정의만 봐도 충분히 이해할 수 있거든요. 배열, 복소수, 유리수 같은 다른 사전 정의 타입들은 2부(Part II)에서 다룬답니다.
6.1 표준 하스켈 타입 (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)이에요. 기본적인 불리언 함수로는 &&(그리고), ||(또는), not이 있어요. otherwise라는 이름은 True로 정의되어 있는데, | 가드를 쓴 표현을 더 읽기 좋게 만들어 주죠.
6.1.2 문자와 문자열 (Characters and Strings)
Char 타입은 그 값들이 유니코드 문자[2]를 나타내는 열거형이에요. 문자의 어휘 문법은 2.6절에 정의되어 있고, 문자 리터럴은 Char 데이터 타입의 인자가 없는 생성자(nullary constructor)가 돼요. Char는 Read, Show, Eq, Ord, Enum, Bounded 클래스의 인스턴스입니다. Enum 클래스의 표준 함수인 toEnum과 fromEnum은 문자를 Int 타입으로·Int 타입에서 문자로 바꿔 주는 역할을 하죠.
한 가지 짚고 갈게요. ASCII 제어 문자는 문자 리터럴 안에서 여러 가지로 표현될 수 있어요 — 숫자 이스케이프, ASCII 니모닉 이스케이프, 그리고 \^X 표기법이 있죠. 추가로 다음과 같은 동치 관계도 있어요. \a와 \BEL, \b와 \BS, \f와 \FF, \r과 \CR, \t와 \HT, \v와 \VT, 그리고 \n과 \LF가 서로 같답니다.
type String = [Char]
문자열은 문자의 리스트예요.
"A string" -- 축약 표현
['A',' ','s','t','r','i','n','g'] -- 위와 같음
문자열은 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절)에는 표준 리스트 함수가 많이 정의되어 있고요. 산술 수열(arithmetic sequence)과 리스트 내포(list comprehension)는 특별한 종류의 리스트를 간편하게 쓰는 두 가지 문법인데, 각각 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는 추상 타입이라 사용자에게 보이는 생성자가 없고요, Monad와 Functor 클래스의 인스턴스입니다. I/O 연산에 대한 설명은 7장에서 다룹니다.
IOError는 I/O 연산이 일으키는 오류를 나타내는 추상 타입이에요. Show와 Eq의 인스턴스이고, 이 타입의 값은 여러 I/O 함수가 만들어 내는데 이 보고서에서는 더 자세히 다루지 않아요. Prelude에는 몇 개의 I/O 함수(9.3절 정의)가 있고, 2부에 훨씬 더 많이 있답니다.
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 타입은 Ord 클래스의 compare가 사용하는 타입이죠. 그리고 maybe와 either 함수가 Prelude에 들어 있어요.
6.2 엄격한 평가 (Strict Evaluation)
seq :: a -> b -> b
Haskell의 함수 적용은 non-strict, 즉 게으르다는 성질이 있어요. 함수 인자는 필요할 때만 평가되죠. 그런데 어떤 값을 억지로 평가해야 할 때가 있거든요. 그럴 때 seq 함수를 써요.
seq는 다음 방정식으로 정의돼요.
seq ⊥ b = ⊥
seq a b = b, -- a ≠ ⊥ 일 때
seq는 보통 불필요한 게으름을 피해서 성능을 높이려고 도입해요. 엄격한 데이터 타입(4.2.1절)은 $! 연산자로 정의되죠. 그런데 seq가 모든 타입에서 쓸 수 있다는 사실이 중요한 의미론적 결과를 가져와요. 그 덕분에 ⊥는 \x -> ⊥와 같지 않게 됐어요 — seq로 둘을 구분할 수 있으니까요. 같은 이유로 seq의 존재는 Haskell의 파라메트릭성(parametricity) 성질을 약화시키기도 한답니다.
infixr 0 $, $!
($), ($!) :: (a -> b) -> a -> b
f $ x = f x
f $! x = x `seq` f x
$! 연산자는 엄격한(값에 의한 호출, call-by-value) 적용이고, seq로 정의돼 있어요. Prelude는 반대로 non-strict 적용을 하는 $ 연산자도 정의하고 있죠.
f $ g $ h x = f (g (h x))
non-strict 적용 연산자 $는 겉보기에 중복돼 보일 수 있어요. 평범한 적용 (f x)가 (f $ x)와 같은 의미니까요. 그런데 $는 낮은 결합 우선순위에 오른쪽 결합이라, 괄호를 생략하게 해 줄 때가 있죠. 예를 들면 이런 식이에요.
이 녀석은 고차(high-order) 상황에서도 유용해요. map ($ 0) xs처럼 쓰거나 zipWith ($) fs xs처럼 쓸 수 있죠.
6.3 표준 하스켈 클래스 (Standard Haskell Classes)
그림 6.1은 Prelude에 정의된 Haskell 클래스들의 계층 구조와, 이 클래스들의 인스턴스가 되는 Prelude 타입을 보여줘요.
그림 6.1: 표준 Haskell 클래스 (Standard Haskell Classes)
표준 클래스의 많은 메서드에는 기본 메서드 선언(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 인스턴스를 파생(derive)할 수 있어요.
이 선언은 /=와 == 양쪽에 기본 메서드 선언을 줘요. 둘 다 서로를 이용해서 정의되죠. 만약 어떤 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 클래스는 전체 순서가 있는 데이터 타입에 쓰여요. 함수, IO, IOError를 빼고 모든 기본 데이터 타입이 인스턴스예요. 구성 요소 타입들이 전부 Ord에 있는 사용자 정의 데이터 타입이면 Ord 인스턴스를 파생할 수 있죠. 데이터 선언에서 생성자가 선언된 순서가 파생된 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
Read와 Show 클래스는 값을 문자열로·문자열에서 값으로 바꾸는 데 쓰여요. showsPrec와 readsPrec에 주어지는 Int 인자는 둘러싼 문맥의 연산자 우선순위를 나타내죠(11.4절 참고).
showsPrec와 showList는 문자열→문자열 함수를 돌려줘요. 그래서 함수 합성으로 결과를 상수 시간에 이어 붙일 수 있죠. show는 이걸 덜어낸 변형인데, 우선순위 문맥 0을 사용하고 평범한 String을 돌려줘요. showList 메서드는 값 리스트를 보여 주는 특별한 방법을 프로그래머가 제공하게 해 줘요. 특히 Char 타입에 유용한데, String 타입의 값은 대괄호 사이가 아니라 큰따옴표 안에 보여 주는 게 자연스럽거든요.
파생된 Read와 Show 인스턴스는 생성자가 선언된 방식을 그대로 재현해요. 즉 중위(infix) 생성자와 필드 이름을 입력·출력에 그대로 쓰죠. showsPrec가 만들어 낸 문자열은 보통 readsPrec가 다시 읽어 들일 수 있어요.
함수 타입과 IO 타입을 제외한 모든 Prelude 타입이 Show와 Read의 인스턴스입니다. (원하면 프로그래머가 인스턴스 선언을 하나 제공해 함수 타입이나 IO 타입을 (공허한) Show 인스턴스로 만들 수도 있어요.)
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"
편의를 위해 Prelude가 다음의 보조 함수들을 제공해요.
shows와 reads는 기본 우선순위 0을 사용해요. read 함수는 문자열에서 입력을 읽는데, 그 문자열이 입력 과정에서 완전히 소모되어야 해요.
read가 사용하는 lex :: ReadS String 함수도 Prelude의 일부예요. 입력에서 어휘소(lexeme) 하나를 읽는데, 앞쪽 공백을 버리고 그 어휘소를 이루는 문자들을 돌려줘요. 입력 문자열이 공백뿐이면 lex는 빈 문자열로 이루어진 성공적인 "어휘소" 하나를 돌려줘요. (그래서 lex "" = [("","")]이 되는 거죠.) 입력 문자열 시작에 쓸 만한 어휘소가 없으면 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 클래스는 순서가 있는 타입에 대한 연산을 정의해요. succ와 pred는 각각 한 값의 다음 값과 이전 값을 돌려주죠. fromEnum과 toEnum은 Enum 타입의 값을 Int로·Int에서 Int로 바꿔 줘요. enumFrom... 메서드들은 산술 수열(3.10절)을 변환할 때 쓰입니다.
Enum 인스턴스는 어떤 열거형 타입(생성자에 필드가 없는 타입)에 대해서도 파생될 수 있어요. 11장을 참고하세요.
Bounded와 Enum 양쪽의 인스턴스인 타입에 대해서는 다음이 성립해야 해요.
succ maxBound와pred minBound호출은 런타임 오류가 나야 해요.- 결과 값이 결과 타입에 표현될 수 없으면
fromEnum과toEnum은 런타임 오류를 일으켜야 해요. 예를 들어toEnum 7 :: Bool은 오류예요.
enumFrom x = enumFromTo x maxBound
enumFromThen x y = enumFromThenTo x y bound
where
bound | fromEnum y >= fromEnum x = maxBound
| otherwise = minBound
enumFrom과 enumFromThen은 이렇게 암묵적인 경계로 정의돼야 해요.
다음 Prelude 타입들이 Enum의 인스턴스예요.
- 열거형 타입:
(),Bool,Ordering. 이 인스턴스들의 의미는 11장에서 정해져요. 예를 들어[LT ..]는 리스트[LT,EQ,GT]예요. - Char: 인스턴스는
Char와Int사이를 변환하는 원시 함수를 바탕으로 9장에 나와요. 예를 들어enumFromTo 'a' 'z'는 알파벳 순서의 소문자 리스트를 뜻하죠. - 숫자 타입:
Int,Integer,Float,Double. 이 인스턴스들의 의미는 바로 아래에서 다뤄요.
네 숫자 타입 모두에서 succ는 1을 더하고 pred는 1을 빼요. fromEnum과 toEnum 변환은 그 타입과 Int 사이를 오가죠. Float와 Double의 경우 소수점 뒤 자릿수가 유실될 수 있어요. Int에 담기에는 너무 큰 값에 fromEnum을 적용하면 무엇이 돌아올지는 구현에 따라 달라요.
Int와 Integer 타입에 대해서는 열거 함수가 다음과 같은 의미를 가져요.
- 수열
enumFrom e1은 리스트[e1, e1+1, e1+2, …]예요. - 수열
enumFromThen e1 e2는 리스트[e1, e1+i, e1+2i, …]인데, 여기서 증가분i는e2 − e1이에요. 증가분은 0이거나 음수일 수 있죠. 증가분이 0이면 모든 리스트 원소가 같아요. - 수열
enumFromTo e1 e3는 리스트[e1, e1+1, e1+2, … , e3]예요.e1 > e3이면 리스트는 비어요. - 수열
enumFromThenTo e1 e2 e3는 리스트[e1, e1+i, e1+2i, … , e3]인데, 증가분i는e2 − e1이에요. 증가분이 양수이거나 0이면 다음 원소가e3보다 커지려는 순간 수열이 끝나요.e1 > e3이면 리스트는 비고요. 증가분이 음수면 다음 원소가e3보다 작아지려는 순간 수열이 끝나요.e1 < e3이면 리스트는 비어요.
Float와 Double의 경우 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 클래스는 뭔가에 매핑할 수 있는 타입에 쓰여요. 리스트, 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 메서드는 빈 리스트 []를 돌려주고, Maybe는 Nothing을 돌려주며, IO는 IO 모나드 안에서 사용자 예외를 던져요(7.3절).
Monad 인스턴스는 다음 법칙을 만족해야 해요.
return a >>= k = k a
m >>= return = m
m >>= (\x -> k x >>= h) = (m >>= k) >>= h
Monad와 Functor 양쪽의 인스턴스는 추가로 다음 법칙도 만족해야 해요.
fmap f xs = xs >>= return . f
Prelude에 정의된 모든 Monad 인스턴스는 이 법칙들을 만족해요.
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
Prelude가 다음 보조 함수들을 제공해요.
6.3.7 Bounded 클래스 (The Bounded Class)
class Bounded a where
minBound, maxBound :: a
Bounded 클래스는 타입의 상한과 하한에 이름을 붙일 때 쓰여요. Ord는 Bounded의 상위 클래스가 아니에요 — 전체 순서가 없는 타입도 상한·하한을 가질 수 있으니까요. Int, Char, Bool, (), Ordering, 그리고 모든 튜플이 Bounded의 인스턴스예요. Bounded 클래스는 어떤 열거형 타입에 대해서도 파생될 수 있어요. 이때 minBound는 데이터 선언에 나열된 첫 번째 생성자, maxBound는 마지막 생성자가 돼요. Bounded는 구성 요소 타입들이 전부 Bounded에 있는, 생성자가 하나뿐인 데이터 타입에서도 파생될 수 있죠.
6.4 숫자 (Numbers)
Haskell은 여러 종류의 숫자를 제공해요. 숫자 타입과 그 연산들은 Common Lisp와 Scheme의 영향을 많이 받았어요. 숫자 함수 이름과 연산자는 대개 오버로드되어 있는데, 그림 6.1에 보이는 포함 관계를 가진 여러 타입 클래스를 사용하죠. 숫자 타입의 클래스인 Num은 Eq의 하위 클래스예요 — 모든 숫자는 동등성 비교가 가능하니까요. Num의 하위 클래스 Real은 Ord의 하위 클래스이기도 해요 — 복소수(Complex 라이브러리에 정의)를 제외하면 나머지 비교 연산이 전부 적용되니까요. Integral 클래스는 유한 범위와 무한 범위의 정수를 모두 담고, Fractional 클래스는 모든 비정수 타입을 담으며, Floating 클래스는 실수와 복소수 모든 부동소수점 타입을 담아요.
Prelude는 가장 기본적인 숫자 타입만 정의해요. 고정 크기 정수(Int), 임의 정밀도 정수(Integer), 단정밀도 부동소수(Float), 배정밀도 부동소수(Double)죠. 유리수나 복소수 같은 다른 숫자 타입은 라이브러리에 정의되어 있어요. 특히 Rational 타입은 Ratio 라이브러리에 정의된 대로, Integer 값 두 개의 비율이에요.
Prelude가 정의하는 기본 부동소수점 연산은 현재의 LIA(Language Independent Arithmetic, 언어 독립 산술) 표준에 부합하지 않아요. 그 표준은 숫자 구조에 훨씬 더 많은 복잡성을 요구해서, 라이브러리로 미뤄두었죠. IEEE 부동소수점 표준의 일부(전부는 아니지만)는 Prelude 클래스 RealFloat에 반영되어 있어요.
표준 숫자 타입은 표 6.1에 정리되어 있어요. 유한 정밀도 정수 타입 Int는 최소한 범위 [−2²⁹, 2²⁹−1]을 담아요. Int가 Bounded 클래스의 인스턴스이므로, maxBound와 minBound로 어떤 구현이 정의하는 정확한 Int 범위를 알아낼 수 있어요. Float는 구현에 따라 정의되며, 범위와 정밀도가 IEEE 단정밀도 타입과 적어도 같게 만드는 것이 바람직해요. Double도 마찬가지로 IEEE 배정밀도를 담아야 하고요. 고정 정밀도 숫자 타입에서 예외 조건(오버플로나 언더플로 같은)의 결과는 정의되어 있지 않아요. 구현이 오류(⊥, 의미상으로)를 택할 수도, 잘린 값을 택할 수도, 무한대나 부정(indefinite) 같은 특수 값을 택할 수도 있어요.
| 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) |
표 6.1: 표준 숫자 타입 (Standard Numeric Types)
Prelude에 정의된 표준 숫자 클래스와 기타 숫자 함수는 그림 6.2~6.3에 나와 있어요. 그림 6.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.2: 표준 숫자 클래스와 관련 연산, Part 1 (Standard Numeric Classes and Related Operations, Part 1)
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.3: 표준 숫자 클래스와 관련 연산, Part 2 (Standard Numeric Classes and Related Operations, Part 2)
6.4.1 숫자 리터럴 (Numeric Literals)
fromInteger :: (Num a) => Integer -> a
fromRational :: (Fractional a) => Rational -> a
숫자 리터럴의 문법은 2.5절에 나와 있어요. 정수 리터럴은 함수 fromInteger를 적절한 Integer 타입 값에 적용한 것을 나타내요. 마찬가지로 부동소수 리터럴은 fromRational을 Rational 타입 값(즉 Ratio Integer)에 적용한 것을 뜻하죠. 위 타이핑이 주어졌을 때:
- 정수 리터럴은
(Num a) => a타입을 갖고요, - 부동소수 리터럴은
(Fractional a) => a타입을 가져요.
숫자 리터럴을 이렇게 간접적으로 정의한 이유는, 어떤 적절한 숫자 타입의 값으로든 해석될 수 있게 하려는 거예요. 오버로딩의 모호함에 대한 논의는 4.3.4절을 참고하세요.
6.4.2 산술과 정수론 연산 (Arithmetic and Number-Theoretic Operations)
(x `quot` y)*y + (x `rem` y) == x
(x `div` y)*y + (x `mod` y) == x
중위 클래스 메서드 (+), (*), (-)와 단항 함수 negate(앞에 붙는 마이너스 기호로도 쓸 수 있어요. 3.4절 참고)는 모든 숫자에 적용돼요. 클래스 메서드 quot, rem, div, mod는 정수 숫자에만 적용되고, (/)는 분수 숫자에만 적용돼요. y가 0이 아닐 때 quot, rem, div, mod 클래스 메서드는 위 법칙을 만족해요.
quotRem x y = (x `quot` y, x `rem` y)
divMod x y = (x `div` y, x `mod` y)
quot는 0 쪽으로 잘라 내리는 정수 나눗셈이에요. 반면 div의 결과는 음의 무한대 쪽으로 잘라 내려요. quotRem 클래스 메서드는 피제수와 제수를 인자로 받아 (몫, 나머지) 순서쌍을 돌려주죠. divMod도 비슷하게 정의돼요.
even x = x `rem` 2 == 0
odd = not . even
정수 숫자에는 even과 odd 술어도 쓸 수 있어요.
마지막으로 최대공약수와 최소공배수 함수가 있어요. gcd x y는 x와 y를 모두 나누는 가장 큰 (양의) 정수예요. 예를 들어 gcd (-3) 6 = 3, gcd (-3) (-6) = 3, gcd 0 4 = 4죠. gcd 0 0은 런타임 오류를 일으켜요.
lcm x y는 x와 y가 모두 나누는 가장 작은 양의 정수예요.
6.4.3 지수와 로그 (Exponentiation and Logarithms)
한 인자 지수 함수 exp와 로그 함수 log는 부동소수점 숫자에 작용하고 밑 e를 사용해요. logBase a x는 밑 a에 대한 x의 로그를 돌려주죠. sqrt는 부동소수점 숫자의 주제곱근(principal square root)을 돌려줘요. 두 인자 지수 연산은 세 가지가 있어요. (^)는 아무 숫자를 음이 아닌 정수 거듭제곱으로 올리고, (^^)는 분수 숫자를 임의의 정수 거듭제곱으로 올리며, (**)는 두 부동소수점 인자를 받아요. 어떤 x에 대해서도 x^0이나 x^^0의 값은 1이에요 — x가 0이어도 말이죠. 0**y는 y가 1이면 1이고, 그 외에는 0이에요.
6.4.4 크기와 부호 (Magnitude and Sign)
abs x * signum x == x
숫자는 크기와 부호를 가져요. 함수 abs와 signum은 아무 숫자에나 적용되고, 위 법칙을 만족해요.
abs x | x >= 0 = x
| x < 0 = -x
signum x | x > 0 = 1
| x == 0 = 0
| x < 0 = -1
실수에 대해서 이 함수들은 이렇게 정의돼요.
6.4.5 삼각 함수 (Trigonometric Functions)
Floating 클래스는 원형·쌍곡선 사인, 코사인, 탄젠트 함수와 그 역함수를 제공해요. tan, tanh, logBase, **, sqrt에는 기본 구현이 제공되지만, 구현자는 더 정확한 구현을 제공할 자유가 있어요.
RealFloat 클래스는 실수 부동소수점 인자 두 개를 받는 아크탄젠트 버전을 제공해요. 실수 부동소수 x와 y에 대해 atan2 y x는 원점에서 점 (x,y)로 가는 벡터의 (양의 x축에서 잰) 각도를 계산해요. atan2 y x는 범위 [−π, π]의 값을 돌려주죠. 부호 있는 0이 지원될 때 원점에 대해서는 Common Lisp 의미를 따르고요. RealFloat 타입의 y에 대해 atan2 y 1은 atan y와 같은 값을 돌려줘야 해요. atan2에도 기본 정의가 제공되지만, 구현자는 더 정확한 구현을 제공할 수 있어요.
위 함수들의 정확한 정의는 Common Lisp에 따르는데, 그 Common Lisp는 다시 APL을 위한 Penfield의 제안[12]을 따르죠. 가지 자름(branch cut), 불연속, 구현에 대한 논의는 그 참고 문헌들을 보시면 돼요.
6.4.6 강제 변환과 성분 추출 (Coercions and Component Extraction)
ceiling, floor, truncate, round 함수는 각각 실수 분수 인자를 받아 정수 결과를 돌려줘요. ceiling x는 x보다 작지 않은 가장 작은 정수이고, floor x는 x보다 크지 않은 가장 큰 정수예요. truncate x는 0과 x 사이(x 포함)에서 x에 가장 가까운 정수를 내놓죠. round x는 x에 가장 가까운 정수를 돌려주는데, x가 두 정수 사이 정중앙에 있으면 짝수를 돌려줘요.
properFraction 함수는 실수 분수 숫자 x를 받아 순서쌍 (n,f)를 돌려줘요. 이때 x = n + f이고, n은 x와 같은 부호의 정수이며, f는 x와 같은 타입·부호를 가진, 절댓값이 1보다 작은 분수예요. ceiling, floor, truncate, round 함수는 전부 properFraction으로 정의할 수 있어요.
숫자를 Rational 타입으로 바꿔 주는 함수가 둘 있어요. toRational은 실수 인자를 전체 정밀도로 바꾼 유리수 동치를 돌려주고, approxRational은 실수 분수 인자 둘 x와 ε를 받아 x로부터 ε 이내에 있는 가장 단순한 유리수를 돌려줘요. 기약분수 형태의 유리수 p/q가 다른 p'/q'보다 단순하다는 건 |p| ≤ |p'|이고 q ≤ q'라는 뜻이에요. 모든 실수 구간에는 유일한 가장 단순한 유리수가 들어 있어요. 특히 0/1이 모든 것 중 가장 단순한 유리수라는 점을 기억해 두시면 좋아요.
RealFloat 클래스의 클래스 메서드는 부동소수점 숫자의 성분에 효율적이고 머신에 독립적으로 접근하게 해 줘요. floatRadix, floatDigits, floatRange는 부동소수점 타입의 매개변수를 알려주죠. 각각 표현의 기수(radix), 가수(significand)에 있는 그 기수의 자릿수, 그리고 지수가 취할 수 있는 최솟값과 최댓값이에요. decodeFloat를 실수 부동소수점 숫자에 적용하면 Integer로 표현된 가수와 적절히 스케일된 지수(Int)를 돌려줘요. decodeFloat x가 (m,n)을 내놓으면 x의 값은 m·(b^n)이에요. 여기서 b는 부동소수점 기수이죠. 게다가 m과 n이 둘 다 0이거나, b^(d−1) ≤ |m| < b^d를 만족해요(여기서 d는 floatDigits x의 값). encodeFloat는 이 변환의 역을 수행해요. significand와 exponent 함수는 함께 decodeFloat와 같은 정보를 주지만, significand x는 Integer 대신 x와 같은 타입의 값을 내놓아요. 그 값은 열린 구간 (1,1)에 있도록 스케일된 거죠. exponent 0은 0이에요. scaleFloat는 부동소수점 숫자에 기수의 정수 거듭제곱을 곱해요.
isNaN, isInfinite, isDenormalized, isNegativeZero, isIEEE 함수는 모두 IEEE 표준으로 표현된 숫자를 지원해요. IEEE가 아닌 부동소수점 숫자에 대해서는 이들이 전부 false를 돌려줄 수 있어요.
fromIntegral :: (Integral a, Num b) => a -> b
realToFrac :: (Real a, Fractional b) => a -> b
또한 다음과 같은 강제 변환 함수도 쓸 수 있어요.
더 알아보기 (Learn more)
- 이 장은 Haskell 2010 Language Report의 6장「Predefined Types and Classes」전체를 번역한 것이에요. 원문 단일 페이지: https://www.haskell.org/onlinereport/haskell2010/haskellch6.html
- 정의는 6.1~6.4절, 클래스는 6.3절, 숫자 체계는 6.4절에서 다뤄요.
- 참조되는 다른 절들: 문법(2.5, 2.6), 산술 수열·리스트 내포(3.10, 3.11), do 표현식(3.14), 숫자 오버로딩 모호성(4.3.4), 기본 메서드(4.3), I/O 연산(7장), 클래스·함수 정의(Prelude, 9장), 파생 인스턴스(11장).
- Prelude가 실제 정의하는 함수 전체는 9장(Prelude)에서 확인할 수 있어요.