티스토리 뷰

유형(類型,type)을 믿어라

 이전에 한 번 Haskell이 계속값을 유지하는 유형(類型,type) 체계(static type system)를 갖고 있다고 언급한 적이 있었지. 모든 표현식의 유형(類型,type)은 기계어로 번역할 때(compile time)에 알 수 있고, 이건 더 안전한 명령어 문장을 만들어줘. 만약 프로그램을 짤 때 boolean 유형(類型,type)을 어떤 숫자로 나누려고 시도한다면, 그건 번역(compile)도 안될 거야. 이게 프로그램을 작동할 때 오류를 발견하는 대신 번역(compile)시간에 더 많은 오류를 잡을 수 있게 해주기 때문에 더 좋아. Haskell에 있는 모든 것들은 유형(類型,type)을 갖고 있고, 따라서 번역기(compiler)는 번역하기 전에 네 프로그램에 대해 더 많은 것을 추론해낼 수 있어.

  Java언어나 Pascal 언어와는 달리, Haskell은 유형(類型,type) 예측(type inference) 기능을 갖고 있어. 숫자를 썼을 때 따로 Haskell에게 그게 숫자라고 알려줄 필요가 없지. Haskell은 스스로 그 유형(類型,type)을 추론해내고, 따라서 우리는 모든 함수와 표현식에 대해 일일히 명시적으로 형(型,형태)을 써줄 필요가 없는거야. 유형(類型,type)에 대해 정말 대충 슥 훑어보는 것만으로도 Haskell의 기초 중 일부분을 다룰 수 있어. 하지만, 유형(類型,type)체계를 이해하는 건 Haskell을 배우는 것에 있어 정말 중요한 부분이야.

 유형(類型,type)은 모든 표현식이 갖고 있는 표시(label)의 일종이야. 유형(類型,type)은 각 표현식이 어울리는 구분이 어떤건지 말해줘. 표현식 True(참)는 Boolean이고, "hello"는 문자열이고, 뭐 그런 식.

 이제 몇몇 표현식들의 유형(類型,type)을 검사하기 위해 GHCI를 사용할 거야. 유효한 표현식 앞에 :t 명령어를 사용하면 해당 표현식의 유형(類型,type)이 뭔지 알 수 있어. 봐 봐.

  1. ghci> :t 'a'  
  2. 'a' :: Char  
  3. ghci> :t True  
  4. True :: Bool  
  5. ghci> :t "HELLO!"  
  6. "HELLO!" :: [Char]  
  7. ghci> :t (True'a')  
  8. (True'a') :: (BoolChar)  
  9. ghci> :t 4 == 5  
  10. 4 == 5 :: Bool  

여기서 표현식에 :t 명령을 사용했을 때 표현식과 ::, 그 뒤에 해당 유형(類型,type)이 뭔지 나오는 걸 볼 수 있어. ::는 "has type of"(~의 유형(類型,type)은)라고 읽어. 명시적인 유형(類型,type)은 항상 첫번째 글자가 대문자로 표기돼. 'a'는 위에도 나와있듯이 Char 유형(類型,type)이지. 이게 문자를 나타내기 위한 유형(類型,type)이라는 결론을 내리긴 어렵지 않을거야. True는 Bool 유형(類型,type)이지. 이건 타당해. 하지만 이건 어때? "HELLO!"의 유형(類型,type)은 [Char]이라고 나와. 대괄호는 목록을 나타내지. 따라서 우리는 이걸 문자의 목록(문자열)으로 읽을 수 있어. 목록과는 다르게, 이종(異種)목록(Tuple)은 길이마다 서로 다른 유형(類型,type)을 갖고 있어. 따라서 (True, 'a')는 (Bool, Char)유형(類型,type)을 갖는 반면에 ('a','b','c')와 같은 표현식은 (Char, Char, Char)유형(類型,type)을 갖겠지. 4==5 는 항상 False를 내보내기 때문에 Bool 유형(類型,type)이야.

 함수도 마찬가지로 유형(類型,type)을 갖고 있어. 함수를 만들 때, 함수의 유형(類型,type) 선언을 명시함으로써 함수의 유형(類型,type)을 선택할 수 있어. 이건 보통 아주 짧은 함수를 쓸 때 말고는 좋은 연습이 돼. 이제부터, 우리는 우리가 만들 모든 함수에 대해 유형(類型,type) 선언을 명시할거야. 이전에 우리가 만든, 문자열에서 대문자인 문자만 남기는 조건 제시형 목록을 기억해? 여기 그 유형(類型,type) 선언이 어떻게 되는 지 나와있어.

  1. removeNonUppercase :: [Char] -> [Char]  
  2. removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]   

 removeNonUppercase는 [Char]->[Char] 유형(類型,type)을 가져. 이건 문자열을 문자열에 대응시킨다는 뜻이야. 왜냐하면 이 함수는 하나의 문자열을 인자로 받아서 다른 문자열 하나를 그 결과를 보내주거든. [Char]유형(類型,type)은 String과 동의어니까, removeNonUppercase :: String -> String라고 쓰는 게 더 명확할거야. 이 함수에 따로 유형(類型,type) 선언을 해 줄 필요는 없어. 왜냐하면 번역기(compiler)가 스스로 이 함수의 형(型)을 알아서 추론해낼 수 있거든. 아무튼 우린 형(型)을 명시했어. 여러 개의 인자를 받는 함수의 형(型)은 어떻게 써야할까? 여기 세 개의 정수를 받아서 그걸 더해 돌려주는 간단한 함수가 있어.

  1. addThree :: Int -> Int -> Int -> Int  
  2. addThree x y z = x + y + z  

 인자들은 ->로 구분되고 인자와 그 결과형(return 型)간에 특별한 구분은 없어. 결과형(return 型)은 유형(類型,type) 선언에서 맨 마지막 요소고 인자는 처음 세 개의 요소야. 나중에 왜 얘네들이 결과형(return 型)과 인자 사이에 Int, Int, Int -> Int같은 어떤 구분 없이 ->로만 구분해서 쓰는지 알아볼거야.

 만약 함수에 어떤 유형(類型,type) 선언을 해주고 싶지만 그게 어떤 유형(類型,type)인지 확신이 안간다면, 그냥 선언 없이 함수를 쓴 다음에 :t 명령으로 그 유형(類型,type)을 확인하면 돼. 함수도 표현식이고, 따라서 :t 명령어는 아무 문제없이 잘 동작해.

 몇가지 일반적인 유형(類型,type)에 대해 간략하게 알아보자.

 Int 는 정수를 나타내는 유형(類型,type)이야. 이건 모든 숫자들을 표현하는데 사용돼. 7은 Int지만 7.2는 아냐. Int는 한계가 있어(bounded). 무슨 뜻이냐 하면 이건 최소값과 최대값이 있다는 거야. 보통 32비트 환경에서 Int의 최대값은 2147483647이고 최솟값은 -2147483648이야.

 Integer 는... 역시 정수를 나타내는 유형(類型,type)이야. 가장 큰 차이는 얘는 한계가 없어서 보통 아주 아주 큰 숫자를 표현하는데 사용된다는 거야. 진짜 진짜 정말로 큰 숫자 말이야. 하지만 Int가 좀 더 효율적이긴 해.

  1. factorial :: Integer -> Integer  
  2. factorial n = product [1..n]  


  1. ghci> factorial 50  
  2. 30414093201713378043612608166064768844377641568960512000000000000  

Float은 보통 정밀도(single precision)의 부동 소수점 숫자야.

  1. circumference :: Float -> Float  
  2. circumference r = 2 * pi * r  


  1. ghci> circumference 4.0  
  2. 25.132742  

Double은 훨씬 정밀한(double the precision) 부동 소수점 숫자지!

  1. circumference' :: Double -> Double  
  2. circumference' r = 2 * pi * r  


  1. ghci> circumference' 4.0  
  2. 25.132741228718345  

Bool 은 논리 유형(類型,type)이야. 이건 True와 Fasle라는 두 가지의 값만 가져.

Char는 문자를 나타내. 이건 홑따옴표로 표기돼. 문자의 목록은 문자열(String)이야.

 이종(異種)목록(Tuple)도 유형(類型,type)이지만 이건 그들의 길이 만큼이나 그 구성요소의 유형(類型,type)에 의존적이기 때문에, 이론적으로는 이종(異種)목록(Tuple)의 유형(類型,type)은 무한히 많이 존재할 수 있고, 이 강의에서 그 모든 걸 다루기엔 숫자가 너무 많아. 한 가지 알아둘 건 비어있는 이종(異種)목록(Tuple) () 또한 유형(類型,type)이고 이건 ()라는 한 가지 값밖에 가질 수 없어.


 형(型) 변수(Type variables)

 head 함수의 유형(類型,type)이 뭐라고 생각해? head는 임의 유형(類型,type)의 목록을 받아서 그 첫번째 원소를 돌려주니까, 어떤 유형(類型,type)이 될까? 확인해봐!

  1. ghci> :t head  
  2. head :: [a] -> a  

 흐으음! a는 뭐지? 이게 유형(類型,type)이야? 이전에 유형(類型,type)은 대문자로 시작한다고 이야기했던 걸 떠올려봐. 이건 대문자로 시작하지 않기 때문에, 실제로는 유형(類型,type) 변수(type variable)야. a는 무슨 유형(類型,type)이든 될 수 있다는 걸 의미하지. 이건 다른 언어의 generic과 많이 비슷해. Haskell에서는 이게 훨씬 강력하게 동작해. 왜냐하면 어떤 함수가 형(型)에 관해 명확한 어떤 동작을 수행하지 않는다면 우리가 훨씬 쉽게 일반적인 함수들을 작성할 수 있게 만들어주기 때문이야. 형(型) 변수를 가진 함수는 다형적 함수(polymorphic functions)라고 불려. head의 형(型) 선언은 head가 임의 형(型)의 목록을 인자로 받아서 해당 형(型)의 원소 하나를 돌려준다고 말하고 있는거야.

 유형(類型,type) 변수 이름이 한 글자 이상이어도 별 상관없지만, 보통 a,b,c,d... 라는 이름을 붙여.

 fst (first) 함수 기억나? 이건 pair의 첫번째 원소를 돌려줘. 얘의 유형(類型,type)이 뭔지 알아보자.

  1. ghci> :t fst  
  2. fst :: (a, b) -> a  

 fst는 두 개의 유형(類型,type)을 가진 이종(異種)목록(Tuple)을 받아서 페어의 첫번째 요소와 같은 유형(類型,type)의 원소를 돌려준다는 걸 알 수 있어. 그래서 fst를 어떤 임의 유형(類型,type)을 가진 페어에 대해서도 쓸 수 있지. a,b가 서로 다른 유형(類型,type) 변수라고 해서 반드시 그 둘이 서로 다른 유형(類型,type)이어야 할 필요는 없다는 걸 명심해둬. 이건 단지 첫번째 요소의 유형(類型,type)과 그 결과 원소의 유형(類型,type)이 같다는 걸 말해주고 있을 뿐이야.


101 형분류(型分類) Typeclasses 101


 형분류(型分類)는 어떤 행동을 정의해놓은 이음자리(interface)의 일종이야. 어떤 유형(類型,type)이 형분류(型分類)에 속한다면, 해당 유형(類型,type)은 형분류(型分類)가 서술하는 행동을 지원하고 수행한다는 의미야. 객체지향 언어를 하다 온 사람들이 형분류(型分類)를 이해하는 것에서 혼란을 많이 겪는데, 왜냐하면 형분류(型分類)가 객체지향 언어에서 반과 비슷한 거라고 생각하기 때문이야. 흠, 근데 그렇지 않아. 넌 이걸 Java의 interface(이음자리)처럼 생각하는게 더 나을거야.

 == 함수의 유형(類型,type)서명(type signature)은 뭘까?

  1. ghci> :t (==)  
  2. (==) :: (Eq a) => a -> a -> Bool  


주: 동등성 연산, == 는 함수야. 따라서 +, *, -, / 그리고 거의 모든 연산들도 함수지. 만약 함수가 특수 문자들로만 구성되어있다면, 이건 기본적으로 중위 함수(infix function)로 여긴다는 거야. 우리가 얘네들의 유형(類型,type)을 알기 원하거나, 다른 함수에 넘기거나 전위 함수(prefix function)로 부르고 싶다면, 이걸 소괄호로 둘러싸야만 해.

 흥미롭지. 여기서 새로운 기호 =>를 발견할 수 있어. => 기호 전에 있는 건 전부 다 반제약(class constraint)이라고 불러. 우린 위의 유형(類型,type) 선언을 이렇게 읽을 수 있어. '동등성 함수는 어떤 두 개의 값을 취하는데, 그 둘은 서로 같은 유형(類型,type)이어야하고 Bool을 내보내(return). 이 두 값의 유형(類型,type)은 반드시 Eq류(類,class)의 구성원(member)여야해 (이 부분이 반제약(class constraint)이야).'

 Eq 유형류(類型類)는 동등성 비교를 위한 이음자리(interface)를 제공해. 두 값이 서로 같은지 비교할 수 있는 유형(類型,type)이면 당연히 Eq류(類,class)의 구성원(member)여야 해. Haskell의 모든 기본 유형(類型,type)은 IO(입출력을 다루기 위한 유형(類型,type))와 함수를 제외하곤 Eq 유형(類型,type)류(類,class)에 속하는 유형(類型,type)들이야.

 elem 함수는 (Eq a) => a -> [a] -> Bool 유형(類型,type)을 가져. 왜냐하면 이 함수는 목록을 돌면서 해당 값이 목록 안에 속해있는지 아닌지를 확인하기 위해 == 함수를 이용하거든.


 몇가지 기본적인 유형류(類型類, typeclass)에 대해 알아보자.

 Eq 는 동등성 비교를 지원하는 유형(類型,type)을 위해 사용돼. 이 류(類,class)의 구성원(member)는 == 함수와 /= 함수를 수행할 수 있어. 따라서 함수의 유형(類型,type) 변수에 Eq류(類,class) 제약이 있다면, 이건 ==나 /= 함수를 그 함수의 정의 내부 어딘가에서 사용한다는 거야. 우리가 이전에 언급한 유형(類型,type)들은 함수를 제외하곤 전부 Eq류(類,class)의 구성원(member)야. 따라서 우린 걔네들의 동등성을 비교할 수 있지.

  1. ghci> 5 == 5  
  2. True  
  3. ghci> 5 /= 5  
  4. False  
  5. ghci> 'a' == 'a'  
  6. True  
  7. ghci> "Ho Ho" == "Ho Ho"  
  8. True  
  9. ghci> 3.432 == 3.432  
  10. True  

 Ord는 순서를 가진 유형(類型,type) 을 위한 거야.

  1. ghci> :t (>)  
  2. (>) :: (Ord a) => a -> a -> Bool  

  지금까지 다룬 유형(類型,type)들은 함수를 제외하곤 전부 Ord 류(類,class)의 구성원(member)야. Ord는 >, <, >=, 그리고 <=와 같은 모든 표준 비교함수들을 다루지. compare 함수는 서로 같은 유형(類型,type)인 두 Ord류(類,class)의 구성원(member)를 받아서 그 순서를 돌려줘. Ordering은 GT, LT, 또는 EQ 값을 가질 수 있는 유형(類型,type)이야. 각각은 Greater than, lesser than과 equal을 의미하지.

 Ord의 구성원(member)가 되려면, 해당 유형(類型,type)은 반드시 Eq 유형(類型,type)류(類,class)의 구성원(member)여야해.

  1. ghci> "Abrakadabra" < "Zebra"  
  2. True  
  3. ghci> "Abrakadabra" `compare` "Zebra"  
  4. LT  
  5. ghci> 5 >= 2  
  6. True  
  7. ghci> 5 `compare` 3  
  8. GT  

 Show 류(類,class)의 구성원(member)는 문자열로 나타낼 수 있어. 지금까지 다룬 유형(類型,type)들은 함수를 제외하곤 전부 Show류(類,class)의 구성원(member)이야. Show유형(類型,type)류(類,class)가 처리하는 가장 많이 쓰이는 함수는 show야. 이 함수는 Show류(類,class)의 구성원(member) 유형(類型,type)인 값을 하나 받아서 그걸 문자열로 돌려줘.

  1. ghci> show 3  
  2. "3"  
  3. ghci> show 5.334  
  4. "5.334"  
  5. ghci> show True  
  6. "True"  

 Read 는 Show 유형(類型,type)류(類,class)와 반대되는 류(類,class)의 일종이야. read 함수는 문자열을 받아서 Read류(類,class)의 구성원(member)인 유형(類型,type)을 돌려줘.

  1. ghci> read "True" || False  
  2. True  
  3. ghci> read "8.2" + 3.8  
  4. 12.0  
  5. ghci> read "5" - 2  
  6. 3  
  7. ghci> read "[1,2,3,4]" ++ [3]  
  8. [1,2,3,4,3]  

 지금까진 아주 좋아. 다시 한 번 말하지만, 지금까지 다룬 모든 유형(類型,type)들은 이 유형(類型,type)류(類,class)에 속해. 하지만 우리가 그냥 read "4" 라고만 적으면 어떤 일이 일어날까?

  1. ghci> read "4"  
  2. <interactive>:1:0:  
  3.     Ambiguous type variable `a' in the constraint:  
  4.       `Read a' arising from a use of `read' at <interactive>:1:0-7  
  5.     Probable fix: add a type signature that fixes these type variable(s)  

 여기서 GHCI가 말하는 건 우리가 뭘 내보내는 지 알 수 없다는 거야. 위에서 우리는 read 함수를 쓰고 이후에 그 결과를 가지고 뭔가 처리를 했지. 이럴 때, GHCI는 우리가 read 함수의 결과로 어떤 유형(類型,type)을 원하는지 추측해내. 만약 우리가 read 함수의 결과를 boolean 유형(類型,type)으로 취급해서 처리했다면, GHCI는 우리가 read 함수의 결과로 Bool을 원한다는 걸 알 수 있어. 하지만 이 경우에는, 우리가 Read류(類,class)의 구성원(member)인 어떤 유형(類型,type)을 원한다는 건 알지만 그게 어떤 건지 알 수가 없어. read 함수의 유형(類型,type)서명을 한 번 살펴보자.

  1. ghci> :t read  
  2. read :: (Read a) => String -> a  

  알겠어? 이건 Read의 구성원(member)인 유형(類型,type)을 내보내 주지만, 우리가 이 결과값을 받아서 어디선가 쓰지 않는다면 그 유형(類型,type)이 뭔지는 알 수가 없어. 따라서 우린 명시적인 유형(類型,type) 주석(type annotations)을 사용해야해. 유형(類型,type) 주석은 표현식의 유형(類型,type)이 어떠해야만 한다고 명시해주는 방법이야. 간단히 표현식의 뒤에 ::과 명시할 유형(類型,type)을 적어주면 돼. 봐봐.

  1. ghci> read "5" :: Int  
  2. 5  
  3. ghci> read "5" :: Float  
  4. 5.0  
  5. ghci> (read "5" :: Float) * 4  
  6. 20.0  
  7. ghci> read "[1,2,3,4]" :: [Int]  
  8. [1,2,3,4]  
  9. ghci> read "(3, 'a')" :: (IntChar)  
  10. (3'a')  

 대부분의 표현식은 번역기(compiler) 스스로 그 유형(類型,type)을 추론해낼 수 있어. 하지만 가끔씩 번역기(compiler)가 read "5"같은 표현식에서 Int 유형(類型,type)을 내보내야할 지 Float 유형(類型,type)을 내보내야 할 지(return) 알 수 없을 때가 있지. 그게 어떤 유형(類型,type)인지 알기 위해 Haskell은 실제로 read "5"를 평가해야만 해. 하지만 haskell은 정적인 유형(類型,type)의 언어이기 때문에, 명령문이 번역(compile)되기 전에(혹은 GHCI의 경우는 식이 평가되기 전에) 모든 형(型)을 알아야돼. 그래서 Haskell한테 "얌마, 이 표현식은 이 형(型)이어야만 돼.혹시 니가 모를 수도 있으니까 알려주는거야!"라고 말해줘야 하는거야.

 Enum류(類,class)의 구성원(member)는 연속적인 순서를 갖는 유형(類型,type)들이야. 이것들은 열거될 수 있지. Enum 유형(類型,type)류(類,class)의 가장 큰 이점은 우리가 그 유형(類型,type)을 목록 범위(list range)에서 사용할 수 있다는 거야. 또 이 류(類,class)의 구성원(member)들은 successors와 predecessors를 정의하고 있어. succ 함수와 pred함수로 각각을 얻을 수 있지. 이 류(類,class)에 속한 유형(類型,type)은 다음과 같아. (), Bool, Char, Ordering, Int, Integer, Float 그리고 Double.

  1. ghci> ['a'..'e']  
  2. "abcde"  
  3. ghci> [LT .. GT]  
  4. [LT,EQ,GT]  
  5. ghci> [3 .. 5]  
  6. [3,4,5]  
  7. ghci> succ 'B'  
  8. 'C'  

 Bounded류(類,class)의 구성원(member)는 상한선과 하한선을 갖고 있어.

  1. ghci> minBound :: Int  
  2. -2147483648  
  3. ghci> maxBound :: Char  
  4. '\1114111'  
  5. ghci> maxBound :: Bool  
  6. True  
  7. ghci> minBound :: Bool  
  8. False  

 minBound 함수와 maxBound 함수는 (Bounded a) => a 라는 유형(類型,type)을 갖고 있어서 흥미로워. 얘네들은 어느정도 다형적인 제약(polymorphic constraints)을 갖고 있지.

 모든 이종(異種)목록(Tuple)들은 그 구성요소들이 Bounded의 구성원(member)라면 자신도 역시 Bounded의 구성원(member)가 돼.

  1. ghci> maxBound :: (BoolIntChar)  
  2. (True,2147483647,'\1114111')  

 Num은 숫자 유형(類型,type)류(類,class)이야. 이 류(類,class)의 구성원(member)는 숫자처럼 동작할 수 있는 특징을 갖고 있어. 숫자의 유형(類型,type)을 한 번 조사해보자.

  1. ghci> :t 20  
  2. 20 :: (Num t) => t  

 모든 숫자들은 역시 다형적인 제약처럼 나타나. 얘네들은 Num의 구성원(member)인 유형(類型,type)이라면 어떤 유형(類型,type)처럼도 행동할 수 있어.

  1. ghci> 20 :: Int  
  2. 20  
  3. ghci> 20 :: Integer  
  4. 20  
  5. ghci> 20 :: Float  
  6. 20.0  
  7. ghci> 20 :: Double  
  8. 20.0  

 이것들이 Num 유형(類型,type)류(類,class)의 구성원(member)들이야. * 함수의 유형(類型,type)을 조사해보면, 이 함수는 모든 숫자들에 대해 동작한다는 걸 볼 수 있어.

  1. ghci> :t (*)  
  2. (*) :: (Num a) => a -> a -> a  

 이 함수는 같은 유형(類型,type)의 숫자 두 개를 받아서 해당 유형(類型,type)의 숫자를 돌려줘. 그래서 (5 :: Int) * (6 :: Integer)는 유형(類型,type) 오류가 발생하지만, 반면에 5 * (6 :: Integer)는 잘 동작하고 그 결과로 Integer 유형(類型,type)을 돌려줘. 5는 Integer로도 Int로도 동작할 수 있거든.

 Num의 구성원(member)가 되기 위해선 해당 유형(類型,type)은 먼저 Show와 Eq의 구성원(member)여야해.

 Integral 유형류(類型類)역시 숫자 유형류(類型類)야. Num은 정수와 실수를 포함한 모든 숫자들을 포함하지. Integral은 모든 정수들만을 포함해. 이 유형류(類型類)에 속한 유형(類型,type)에는 Int와 Integer가 있어.

 Floating은 부동소수점수들만 포함해. 그래서 Float과 Double만 이 유형류(類型類)에 속하지.

 숫자를 다루는 아주 유용한 함수중에 fromIntegral 이 있어. 이건 fromIntegral :: (Num b, Integral a) => a -> b 라는 형(型) 서명을 가지고 있지. 이 유형(類型,type) 서명으로부터 우리는 이게 정수를 받아서 그걸 좀 더 일반적인 숫자로 바꿔주는 역할을 한다는 걸 알 수 있어. 이건 정수와 부동 소수점 유형(類型,type)을 서로 같이 잘 동작하게 만들고 싶을 때 유용해. 한 예로, length 함수는 좀 더 일반적인 형(型)인 (Num b)=> length :: [a] ->b가 아니라 length :: [a] -> Int 라는 유형(類型,type)서명을 갖고 있어. 내 생각에 이건 뭔가 역사적 이유나 어떤 다른게 있었던 것 같애. 내 의견이긴 하지만 이렇게 만들어 놓은 건 정말 바보같은 짓이야. 어쨌든, 우리가 list의 길이를 구해서 그걸 3.2에 더하려고 한다면, Int와 부동 소수점 수를 더하려고 한 것이기 때문에 오류를 발생시켜. 그래서 이걸 해결하기 위해, fromIntegral (length [1,2,3,4]) + 3.2라고 쓰면 잘 동작해.

 fromIntegral은 그 유형(類型, type) 서명에 여러 개의 류(類,class) 제약을 갖는다는 걸 기억해둬. 위에서 봤듯이 이건 완전히 정당한 표현이고, 류(類,class) 제약은 소괄호 안에서 표쉼(,)를 이용해 구분돼.

Types and Typeclasses

Believe the type

Previously we mentioned that Haskell has a static type system. The type of every expression is known at compile time, which leads to safer code. If you write a program where you try to divide a boolean type with some number, it won't even compile. That's good because it's better to catch such errors at compile time instead of having your program crash. Everything in Haskell has a type, so the compiler can reason quite a lot about your program before compiling it.

Unlike Java or Pascal, Haskell has type inference. If we write a number, we don't have to tell Haskell it's a number. It can inferthat on its own, so we don't have to explicitly write out the types of our functions and expressions to get things done. We covered some of the basics of Haskell with only a very superficial glance at types. However, understanding the type system is a very important part of learning Haskell.

A type is a kind of label that every expression has. It tells us in which category of things that expression fits. The expressionTrue is a boolean, "hello" is a string, etc.

Now we'll use GHCI to examine the types of some expressions. We'll do that by using the :t command which, followed by any valid expression, tells us its type. Let's give it a whirl.

  1. ghci> :t 'a'  
  2. 'a' :: Char  
  3. ghci> :t True  
  4. True :: Bool  
  5. ghci> :t "HELLO!"  
  6. "HELLO!" :: [Char]  
  7. ghci> :t (True'a')  
  8. (True'a') :: (BoolChar)  
  9. ghci> :t 4 == 5  
  10. 4 == 5 :: Bool  

Here we see that doing :t on an expression prints out the expression followed by :: and its type. :: is read as "has type of". Explicit types are always denoted with the first letter in capital case. 'a', as it would seem, has a type of Char. It's not hard to conclude that it stands forcharacterTrue is of a Bool type. That makes sense. But what's this? Examining the type of"HELLO!" yields a [Char]. The square brackets denote a list. So we read that as it being a list of characters. Unlike lists, each tuple length has its own type. So the expression of(True, 'a') has a type of (Bool, Char), whereas an expression such as ('a','b','c')would have the type of (Char, Char, Char)4 == 5 will always return False, so its type is Bool.

Functions also have types. When writing our own functions, we can choose to give them an explicit type declaration. This is generally considered to be good practice except when writing very short functions. From here on, we'll give all the functions that we make type declarations. Remember the list comprehension we made previously that filters a string so that only caps remain? Here's how it looks like with a type declaration.

  1. removeNonUppercase :: [Char] -> [Char]  
  2. removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]   

removeNonUppercase has a type of [Char] -> [Char], meaning that it maps from a string to a string. That's because it takes one string as a parameter and returns another as a result. The [Char] type is synonymous with String so it's clearer if we write removeNonUppercase :: String -> String. We didn't have to give this function a type declaration because the compiler can infer by itself that it's a function from a string to a string but we did anyway. But how do we write out the type of a function that takes several parameters? Here's a simple function that takes three integers and adds them together:

  1. addThree :: Int -> Int -> Int -> Int  
  2. addThree x y z = x + y + z  

The parameters are separated with -> and there's no special distinction between the parameters and the return type. The return type is the last item in the declaration and the parameters are the first three. Later on we'll see why they're all just separated with -> instead of having some more explicit distinction between the return types and the parameters likeInt, Int, Int -> Int or something.

If you want to give your function a type declaration but are unsure as to what it should be, you can always just write the function without it and then check it with :t. Functions are expressions too, so :t works on them without a problem.

Here's an overview of some common types.

Int stands for integer. It's used for whole numbers. 7 can be an Int but 7.2 cannot. Int is bounded, which means that it has a minimum and a maximum value. Usually on 32-bit machines the maximum possible Int is 2147483647 and the minimum is -2147483648.

Integer stands for, er … also integer. The main difference is that it's not bounded so it can be used to represent really really big numbers. I mean like really big. Int, however, is more efficient.

  1. factorial :: Integer -> Integer  
  2. factorial n = product [1..n]  
  1. ghci> factorial 50  
  2. 30414093201713378043612608166064768844377641568960512000000000000  

Float is a real floating point with single precision.

  1. circumference :: Float -> Float  
  2. circumference r = 2 * pi * r  
  1. ghci> circumference 4.0  
  2. 25.132742  

Double is a real floating point with double the precision!

  1. circumference' :: Double -> Double  
  2. circumference' r = 2 * pi * r  
  1. ghci> circumference' 4.0  
  2. 25.132741228718345  

Bool is a boolean type. It can have only two values: True and False.

Char represents a character. It's denoted by single quotes. A list of characters is a string.

Tuples are types but they are dependent on their length as well as the types of their components, so there is theoretically an infinite number of tuple types, which is too many to cover in this tutorial. Note that the empty tuple () is also a type which can only have a single value: ()

Type variables

What do you think is the type of the head function? Because head takes a list of any type and returns the first element, so what could it be? Let's check!

  1. ghci> :t head  
  2. head :: [a] -> a  

Hmmm! What is this a? Is it a type? Remember that we previously stated that types are written in capital case, so it can't exactly be a type. Because it's not in capital case it's actually a type variable. That means that a can be of any type. This is much like generics in other languages, only in Haskell it's much more powerful because it allows us to easily write very general functions if they don't use any specific behavior of the types in them. Functions that have type variables are called polymorphic functions. The type declaration of head states that it takes a list of any type and returns one element of that type.

Although type variables can have names longer than one character, we usually give them names of a, b, c, d …

Remember fst? It returns the first component of a pair. Let's examine its type.

  1. ghci> :t fst  
  2. fst :: (a, b) -> a  

We see that fst takes a tuple which contains two types and returns an element which is of the same type as the pair's first component. That's why we can use fst on a pair that contains any two types. Note that just because a and b are different type variables, they don't have to be different types. It just states that the first component's type and the return value's type are the same.

Typeclasses 101

A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes. A lot of people coming from OOP get confused by typeclasses because they think they are like classes in object oriented languages. Well, they're not. You can think of them kind of as Java interfaces, only better.

What's the type signature of the == function?

  1. ghci> :t (==)  
  2. (==) :: (Eq a) => a -> a -> Bool  
Note: the equality operator, == is a function. So are +*-/ and pretty much all operators. If a function is comprised only of special characters, it's considered an infix function by default. If we want to examine its type, pass it to another function or call it as a prefix function, we have to surround it in parentheses.

Interesting. We see a new thing here, the => symbol. Everything before the => symbol is called a class constraint. We can read the previous type declaration like this: the equality function takes any two values that are of the same type and returns aBool. The type of those two values must be a member of the Eq class (this was the class constraint).

The Eq typeclass provides an interface for testing for equality. Any type where it makes sense to test for equality between two values of that type should be a member of the Eq class. All standard Haskell types except for IO (the type for dealing with input and output) and functions are a part of the Eq typeclass.

The elem function has a type of (Eq a) => a -> [a] -> Bool because it uses == over a list to check whether some value we're looking for is in it.

Some basic typeclasses:

Eq is used for types that support equality testing. The functions its members implement are == and /=. So if there's an Eqclass constraint for a type variable in a function, it uses == or /= somewhere inside its definition. All the types we mentioned previously except for functions are part of Eq, so they can be tested for equality.

  1. ghci> 5 == 5  
  2. True  
  3. ghci> 5 /= 5  
  4. False  
  5. ghci> 'a' == 'a'  
  6. True  
  7. ghci> "Ho Ho" == "Ho Ho"  
  8. True  
  9. ghci> 3.432 == 3.432  
  10. True  

Ord is for types that have an ordering.

  1. ghci> :t (>)  
  2. (>) :: (Ord a) => a -> a -> Bool  

All the types we covered so far except for functions are part of OrdOrd covers all the standard comparing functions such as><>= and <=. The compare function takes two Ord members of the same type and returns an ordering. Ordering is a type that can be GTLT or EQ, meaning greater thanlesser than and equal, respectively.

To be a member of Ord, a type must first have membership in the prestigious and exclusive Eq club.

  1. ghci> "Abrakadabra" < "Zebra"  
  2. True  
  3. ghci> "Abrakadabra" `compare` "Zebra"  
  4. LT  
  5. ghci> 5 >= 2  
  6. True  
  7. ghci> 5 `compare` 3  
  8. GT  

Members of Show can be presented as strings. All types covered so far except for functions are a part of Show. The most used function that deals with the Show typeclass is show. It takes a value whose type is a member of Show and presents it to us as a string.

  1. ghci> show 3  
  2. "3"  
  3. ghci> show 5.334  
  4. "5.334"  
  5. ghci> show True  
  6. "True"  

Read is sort of the opposite typeclass of Show. The read function takes a string and returns a type which is a member ofRead.

  1. ghci> read "True" || False  
  2. True  
  3. ghci> read "8.2" + 3.8  
  4. 12.0  
  5. ghci> read "5" - 2  
  6. 3  
  7. ghci> read "[1,2,3,4]" ++ [3]  
  8. [1,2,3,4,3]  

So far so good. Again, all types covered so far are in this typeclass. But what happens if we try to do just read "4"?

  1. ghci> read "4"  
  2. <interactive>:1:0:  
  3.     Ambiguous type variable `a' in the constraint:  
  4.       `Read a' arising from a use of `read' at <interactive>:1:0-7  
  5.     Probable fix: add a type signature that fixes these type variable(s)  

What GHCI is telling us here is that it doesn't know what we want in return. Notice that in the previous uses of read we did something with the result afterwards. That way, GHCI could infer what kind of result we wanted out of our read. If we used it as a boolean, it knew it had to return a Bool. But now, it knows we want some type that is part of the Read class, it just doesn't know which one. Let's take a look at the type signature of read.

  1. ghci> :t read  
  2. read :: (Read a) => String -> a  

See? It returns a type that's part of Read but if we don't try to use it in some way later, it has no way of knowing which type. That's why we can use explicit type annotations. Type annotations are a way of explicitly saying what the type of an expression should be. We do that by adding :: at the end of the expression and then specifying a type. Observe:

  1. ghci> read "5" :: Int  
  2. 5  
  3. ghci> read "5" :: Float  
  4. 5.0  
  5. ghci> (read "5" :: Float) * 4  
  6. 20.0  
  7. ghci> read "[1,2,3,4]" :: [Int]  
  8. [1,2,3,4]  
  9. ghci> read "(3, 'a')" :: (IntChar)  
  10. (3'a')  

Most expressions are such that the compiler can infer what their type is by itself. But sometimes, the compiler doesn't know whether to return a value of type Int or Float for an expression like read "5". To see what the type is, Haskell would have to actually evaluate read "5". But since Haskell is a statically typed language, it has to know all the types before the code is compiled (or in the case of GHCI, evaluated). So we have to tell Haskell: "Hey, this expression should have this type, in case you don't know!".

Enum members are sequentially ordered types — they can be enumerated. The main advantage of the Enum typeclass is that we can use its types in list ranges. They also have defined successors and predecesors, which you can get with the succand pred functions. Types in this class: ()BoolCharOrderingIntIntegerFloat and Double.

  1. ghci> ['a'..'e']  
  2. "abcde"  
  3. ghci> [LT .. GT]  
  4. [LT,EQ,GT]  
  5. ghci> [3 .. 5]  
  6. [3,4,5]  
  7. ghci> succ 'B'  
  8. 'C'  

Bounded members have an upper and a lower bound.

  1. ghci> minBound :: Int  
  2. -2147483648  
  3. ghci> maxBound :: Char  
  4. '\1114111'  
  5. ghci> maxBound :: Bool  
  6. True  
  7. ghci> minBound :: Bool  
  8. False  

minBound and maxBound are interesting because they have a type of (Bounded a) => a. In a sense they are polymorphic constants.

All tuples are also part of Bounded if the components are also in it.

  1. ghci> maxBound :: (BoolIntChar)  
  2. (True,2147483647,'\1114111')  

Num is a numeric typeclass. Its members have the property of being able to act like numbers. Let's examine the type of a number.

  1. ghci> :t 20  
  2. 20 :: (Num t) => t  

It appears that whole numbers are also polymorphic constants. They can act like any type that's a member of the Numtypeclass.

  1. ghci> 20 :: Int  
  2. 20  
  3. ghci> 20 :: Integer  
  4. 20  
  5. ghci> 20 :: Float  
  6. 20.0  
  7. ghci> 20 :: Double  
  8. 20.0  

Those are types that are in the Num typeclass. If we examine the type of *, we'll see that it accepts all numbers.

  1. ghci> :t (*)  
  2. (*) :: (Num a) => a -> a -> a  

It takes two numbers of the same type and returns a number of that type. That's why (5 :: Int) * (6 :: Integer) will result in a type error whereas 5 * (6 :: Integer) will work just fine and produce an Integer because 5 can act like anInteger or an Int.

To join Num, a type must already be friends with Show and Eq.

Integral is also a numeric typeclass. Num includes all numbers, including real numbers and integral numbers, Integralincludes only integral (whole) numbers. In this typeclass are Int and Integer.

Floating includes only floating point numbers, so Float and Double.

A very useful function for dealing with numbers is fromIntegral. It has a type declaration offromIntegral :: (Num b, Integral a) => a -> b. From its type signature we see that it takes an integral number and turns it into a more general number. That's useful when you want integral and floating point types to work together nicely. For instance, the length function has a type declaration of length :: [a] -> Int instead of having a more general type of(Num b) => length :: [a] -> b. I think that's there for historical reasons or something, although in my opinion, it's pretty stupid. Anyway, if we try to get a length of a list and then add it to 3.2, we'll get an error because we tried to add together anInt and a floating point number. So to get around this, we do fromIntegral (length [1,2,3,4]) + 3.2 and it all works out.

Notice that fromIntegral has several class constraints in its type signature. That's completely valid and as you can see, the class constraints are separated by commas inside the parentheses.

 

- 원본 : http://learnyouahaskell.com/chapters  Learn You a Haskell For Great Good! (CCL 3.0), 한글 번역 : http://jwvg0425.tistory.com

- 재배포 : 일부 수정후 재배포(12000@12000.co) *번역은 제멋대로 한글화

- 중국어 용어 참조 : http://cnhaskell.com/


댓글
공지사항
최근에 달린 댓글
Total
4,965
Today
0
Yesterday
0