User Tools

Site Tools


study:haskell:ch6

Chapter 6. Using Typeclasses


요약 : 함수에 인자로 특정타입이 아니라, generic 타입을 받을 수 있게 해준다. 타입클래스 정의시 같이 구현된 함수들에 대해서, 이 함수들은 (해당 타입클래스에 포함된 타입들에 대해서) 인자다형성을 갖게된다.


typeclasses 필요성


함수의 인자타입에 다형성을 넣어보자.

예를들어, 하스켈에 == 연산자가 없어서 구현해야 한다고 생각해보자.

data Color = Red | Green | Blue

equal :: Color -> Color -> Bool
equal Red Red = True
equal Green Green = True
equal Blue Blue = True
equal _ _ = False
*Main> equal Red Red
True

잘 동작한다.

다른타입 Book 에 대해서도 추가 구현해보자.

...
data Book = History | Math | Computer

equal :: Book -> Book -> Bool
equal History History = True
equal Math Math = True
equal Computer Computer = True
equal _ _ = False
*Main> :load color
[1 of 1] Compiling Main             ( color.hs, interpreted )

color.hs:12:1:
    Duplicate type signatures for `equal'
    at color.hs:3:1-5
       color.hs:12:1-5

color.hs:13:1:
    Multiple declarations of `equal'
    Declared at: color.hs:4:1
                 color.hs:13:1
Failed, modules loaded: none.

함수 이름이 중복된다고 에러가 발생했다.

...
data Book = History | Math | Computer

equal2 :: Book -> Book -> Bool
equal2 History History = True
equal2 Math Math = True
equal2 Computer Computer = True
equal2 _ _ = False

함수명을 바꾸면 잘 컴파일된다.

여기서부터 문제가 발생하는데,

같은 의미를 지닌 equal 연산을 수행하기 위해서 각 타입마다 다른이름의 함수를 만들어야할 위기이다!

'특정 타입' 이 아니라 '특정 범위의 타입('generic')' 한 함수를 만드려면,

TypeClass 를 사용해야한다.


타입클래스는 무엇일까?


일단 결과부터 보자. 위의 equal 을 구현하기 위한 타입클래스 사용방법이다.

data Color = Red | Green | Blue
data Book = History | Math | Computer

class BasicEq a where
    isEqual :: a -> a -> Bool
 
instance BasicEq Color where
    isEqual Red Red = True
    isEqual Green Green = True
    isEqual Blue Blue = True
    isEqual _ _ = False
    
instance BasicEq Book where
    isEqual History History = True
    isEqual Math Math = True
    isEqual Computer Computer = True
    isEqual _ _ = False

ghci 테스트

*Main> isEqual Red Blue
False
*Main> isEqual Red Red
True
*Main> isEqual History Math
False
*Main> isEqual Math Math
True

이제 isEqual 함수로 Color 타입이든, Book 타입이든 적용할 수 있게 되었다!

자, 이제 설명을 시작해보자.

class BasicEq a where
    isEqual :: a -> a -> Bool

맨 위에 Color, Book 타입 선언부는 빼고, BasicEq 타입클래스 선언부터 보자.

이 타입클래스는 isEqual 함수를 정의하고 있다.

즉, isEqual 함수는 BasicEq 에 속하는 타입에 대해서는 적용가능하다.

그럼, 어떻게 특정타입을 BasicEq 타입클래스에 속하게 만들 수 있을까?

아래 코드에 나오는 instance 를 통해 가능하다.

instance BasicEq Color where
    isEqual Red Red = True
    isEqual Green Green = True
    isEqual Blue Blue = True
    isEqual _ _ = False
    
instance BasicEq Book where
    isEqual History History = True
    isEqual Math Math = True
    isEqual Computer Computer = True
    isEqual _ _ = False

이렇게 instance 를 통해서 isEqual 함수를 Color 에 대해서, Book 에 대해서 구현했다.

top level 에서 정의했었을때는 함수이름 중복이라고 에러가 발생했는데,

타입클래스의 instance 안에서 구현했기 때문에 함수 중복 에러를 피할 수 있었다!!

마치 함수의 인자 다형성을 추가한 overloading 을 위해서 타입클래스가 존재하는 듯 하다.


타입클래스에서 디폴트 구현


요약 한줄 : 타입클래스 정의시, 함수의 타입 시그니처 말고도 디폴트로 구현부를 넣을 수 있다.

위 BasicEq 타입클래스에 현재 isEqual 함수만 들어가 있는데,

이 타입 클래스에 isNotEqaul 함수도 포함시키고 싶다.

일단…. 타입클래스 정의는 아래와 같을 것이고…

class BasicEq a where
    isEqual :: a -> a -> Bool
    isNotEqual :: a -> a -> Bool

그리고 이제 아까 봤던 instance 부분에 각 타입에 대하여 isNotEqual 을 구현해주면 된다.

… 하지만. 귀찮다. -_-. isEqual 의 반대가 isNotEqual 이 아닌가??

이럴땐, 타입클래스의 '디폴트 구현' 을 사용하면 된다!

타입클래스를 다시 정의해보자.

class BasicEq a where
    isEqual :: a -> a -> Bool
    isEqual x y = not (isNotEqual x y)

    isNotEqual :: a -> a -> Bool
    isNotEqual x y = not (isEqual x y)

타입클래스 함수의 시그니처 밑에 '디폴트 구현'이 추가되었다.

디폴트 구현 내용을 보면 isEqual 의 결과는 isNotEqual 결과의 반대값이고,

isNotEqual 결과는 isEqual 결과의 반대값으로 정의되어있다.

instance 구현시 저 둘 중 하나만 구현해도 나머지는 '디폴트 구현'에 의해서 구할 수 있게된다.

자, 사용자 정의로 하스켈의 하스켈의 ==, /= 연산자와 매우 비슷한 함수를 완성했다.

사실, Built-in ==, /= 연산자들은 BasicEq 의 디폴트 구현과 거의 동일하게 정의되어있다.

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

Built-in 타입클래스를 살펴보자


이제부턴, 하스켈에 이미 구현되어 있으며, 자주 쓰이는 '타입클래스'들을 살펴보자.


Show


Show 타입클래스는 값을 String 로 바꾸는데 사용한다.

가장 중요한 Show 의 함수는 'show' 입니다.

1개 인자를 받아서 String 으로 변환해줍니다.

ghci> :type show
show :: (Show a) => a -> String

ghci> show 1
"1"

ghci> show [1, 2, 3]
"[1,2,3]"

ghci> show (1, 2)
"(1,2)"

ghci 에 표시된 결과물(문자열)이 다시 하스켈 프로그램으로 들어갈 수 있습니다.

ghci> putStrLn (show 1)
1

ghci> putStrLn (show [1,2,3])
[1,2,3]

* 여기서 잠깐! putStrLn 으로 읽어들인 값을 보면, show 의 리턴값인 문자열을 읽어서 Int 값과 [Int] 값으로 데이터를 읽어들인 걸까요??

*Main> let a = putStrLn (show 1)
*Main> a
1
*Main> :type a
a :: IO ()

헉! IO () 타입!?! 모나드… 입출력은 다음 챕터에 나오니 여기선 패스 -_-;

다시 본론으로 돌아와서…

String 타입 값에 show 를 적용하면 결과가 조금 이상하게 나온다.

ghci> show "Hello!"
"\"Hello!\""

ghci> putStrLn (show "Hello!")
"Hello!"

ghci> show ['H', 'i']
"\"Hi\""

ghci> putStrLn (show "Hi")
"Hi"

ghci> show "Hi, \"Jane\""
"\"Hi, \\\"Jane\\\"\""

ghci> putStrLn (show "Hi, \"Jane\"")
"Hi, \"Jane\""

일단 확인하고 넘어가야 할점이 String == [Char] 라는 것이다.

*Main> ['a','s','d','f'] == "asdf"
True
*Main> let s = ['a','s','d','f']
*Main> s
"asdf"

*Main> show ['H', 'i']
"\"Hi\""
*Main> show "Hi"
"\"Hi\""
*Main> 

나중에 설명이 나오지만, show 의 경우 인자가 문자열이면, 출력시 “” 을 첨가해서 보여준다.

그래서 show [1,2,3] 을 치게 되면 문자열 값인 [1,2,3] 으로 바뀌게 된다.

그런데! ghci 가 내부적으로 출력을 할때 show 함수를 이용해서 출력을 한다.

                         평가후                                                             ghci 의 show : 문자열일 경우 특별히 "" 로 감싸준다.
(show [1,2,3])        ->                        [1,2,3] - 문자열타입                ->               "[1,2,3]"



                        평가후: 문자열이까 특별히 "" 추가                    ghci 의 show : 문자열일 경우 특별히 "" 로 감싸준다.
(show "Hi")            ->                                  "Hi"                               ->              "\"Hi\""
== (show ['H','i'])

Read


Show 의 반대편에 있는 Read 는 String 을 받아서 파싱하고, 데이터를 반환한다.

가장 유용한 함수는 read 함수이다.

ghci> :type read
read :: (Read a) => String -> a
-- file: ch06/read.hs
main = do
        putStrLn "Please enter a Double:"
        inpStr <- getLine
        let inpDouble = (read inpStr)::Double
        putStrLn ("Twice " ++ show inpDouble ++ " is " ++ show (inpDouble * 2))

위의 예제 파일을 보면,

특이한 점은 3번째 줄에 ::Double 로 타입을 명시하고 있다.

Read 와 Show 둘다 instance 가 정의된 많은 타입들이 있는데, 명확한 타입을 알수 없다면, 컴파일러는 많은 타입들 중 하나를 골라야합니다.

이런 경우에는, 종종 Integer 를 선택합니다. floating-point 로 입력받고 싶다면 위에서 처럼 명확한 타입을 명시해야합니다.


노트! about deafulting 대부분의 경우, Double 타입 명시를 생략하게되면, 컴파일러는 타입 추측을 거부하고 에러를 냅니다. 디폴트 구현이 Integer 이여서, 1.1 로 하면 파싱에러가 납니다.


ghci 에서 read 를 사용할때도 같습니다. ghci 가 show 를 사용해서 결과를 출력합니다. read 의 결과는 명시적 타입을 제시해야합니다.

ghci> read "5"

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

ghci> :type (read "5")
(read "5") :: (Read a) => a

ghci> (read "5")::Integer
5

ghci> (read "5")::Double
5.0

ghci> (read "5.0")::Double
5.0

ghci> (read "5.0")::Integer
*** Exception: Prelude.read: no parse

Read 타입클래스는 꽤 복잡한 파서들을 가지고 있다.

readsPrec 함수 구현을 통해 간단한 파서를 정의할 수 있다.

아래 예제 구현.

-- file: ch06/eqclasses.hs
instance Read Color where
    -- readsPrec is the main function for parsing input
    readsPrec _ value = 
        -- We pass tryParse a list of pairs.  Each pair has a string
        -- and the desired return value.  tryParse will try to match
        -- the input to one of these strings.
        tryParse [("Red", Red), ("Green", Green), ("Blue", Blue)]
        where tryParse [] = []    -- If there is nothing left to try, fail
              tryParse ((attempt, result):xs) =
                      -- Compare the start of the string to be parsed to the
                      -- text we are looking for.
                      if (take (length attempt) value) == attempt
                         -- If we have a match, return the result and the
                         -- remaining input
                         then [(result, drop (length attempt) value)]
                         -- If we don't have a match, try the next pair
                         -- in the list of attempts.
                         else tryParse xs
ghci> (read "Red")::Color
Red

ghci> (read "Green")::Color
Green

ghci> (read "Blue")::Color
Blue

ghci> (read "[Red]")::[Color]
[Red]

ghci> (read "[Red,Red,Blue]")::[Color]
[Red,Red,Blue]

ghci> (read "[Red, Red, Blue]")::[Color]
*** Exception: Prelude.read: no parse

마지막 테스트의 에러는, 공백때문이다. 스페이스를 처리하도록 수정하면된다. Read instance 수정할때 앞에오는 공백을 제거하도록 하면된다.

실제로 Read 그다지 널리 쓰이진 않는다.

Read 타입클래스를 통해 복잡한 파서를 만들 수는 있지만, 대부분의 사람들은 Parsec 을 사용하는 더 쉬운 방법을 쓴다. (Parsec 은 챕터16. Using Parsec 에서 자세히 나온다.)


Serialization with Read and Show


read 와 show 함수가 serialization 의 훌륭한 도구들이다.

show 의 결과 대부분은 또한 하스켈에 유요한 문법 형태이다.

(물론 사람들이 show instance 작성시 그렇게 의도로 작성해야하지만)


Parsing large strings


하스켈에서 문자열 핸들링은 보통 lazy 하게 처리한다. 따라서 read 와 show 함수는 꽤 큰 데이터도 무리없이 처리할 수 있다. built-in read 와 show 는 효율적이고 pure 하게 구현되어 있다. (파싱의 예외처리는 ch19. Error Handling 에서 다룬다.)

아래 예제를 살펴보자.

ghci> let d1 = [Just 5, Nothing, Nothing, Just 8, Just 9]::[Maybe Int]
ghci> putStrLn (show d1)
[Just 5,Nothing,Nothing,Just 8,Just 9]

ghci> writeFile "test" (show d1)

ghci> input <- readFile "test"
"[Just 5,Nothing,Nothing,Just 8,Just 9]"

ghci> let d2 = read input

<interactive>:1:9:
    Ambiguous type variable `a' in the constraint:
      `Read a' arising from a use of `read' at <interactive>:1:9-18
    Probable fix: add a type signature that fixes these type variable(s)

ghci> let d2 = (read input)::[Maybe Int]

ghci> print d1
[Just 5,Nothing,Nothing,Just 8,Just 9]

ghci> print d2
[Just 5,Nothing,Nothing,Just 8,Just 9]

ghci> d1 == d2
True

위에서 그냥 read input 을 d2 변수에 할당하려고 할때 에러가 났다. 인터프리터가 d2 의 타입을 알 수 없어서 그렇다. 명확한 타입을 제공하면 된다.

매우 많은 타입들에 대해 Read, Show 의 instance 가 디폴트로 구현되어 있기 때문에 복잡한 데이터 구조에서도 사용 가능하다. 아래는 좀 복잡한 데이터 구조에 대한 시도이다.

ghci> putStrLn $ show [("hi", 1), ("there", 3)]
[("hi",1),("there",3)]

ghci> putStrLn $ show [[1, 2, 3], [], [4, 0, 1], [], [503]]
[[1,2,3],[],[4,0,1],[],[503]]

ghci> putStrLn $ show [Left 5, Right "three", Left 0, Right "nine"]
[Left 5,Right "three",Left 0,Right "nine"]

ghci> putStrLn $ show [Left 0, Right [1, 2, 3], Left 5, Right []]
[Left 0,Right [1,2,3],Left 5,Right []]

* 잠시 . (dot) 와 $ (dollar sign) 을 비교하고 넘어가자.

$ : 괄호 쓰기 귀찮을때.
  putStrLn (show (1 + 1))
  putStrLn (show $ 1 + 1)
  putStrLn (show $ 1 + 1)
  putStrLn $ show $ 1 + 1
. : function chain
    (각 차례대로 입력-출력 타입이 맞아야함)
  putStrLn (show (1 + 1))
  (putStrLn . show) (1 + 1)
  putStrLn . show $ 1 + 1

————————————————————————

Numeric Types


하스켈은 숫자형 타입에 강력하다.

32-bit 나 64-bit integer 부터 임의의 정확도를 가진 실수까지 가능하다.

아마 이미 + 같은 연산자가 모든 숫자형 타입에 적용이 가능한 이유가 타입클래스를 통해서 구현했기 때문임을 알 수 있을 것이다.

추가적으로 사용자 정의 숫자 타입을 정의해서 이를 하스켈의 first-class citizen 으로 만들 수 있다는 점이다.

숫자형 타입의 타입클래스부터 살펴보자. 아래 표는 가장 흔히 쓰이는 숫자 타입들이다. (C 와 상호작용 가능한 특수한 숫자타입들도 더 있다.)

…숫자형 타입…겁나 많음 -_-;

표는 각자 확인해보자;;;


Table 6.1. Selected Numeric Types

Type Description Double Double-precision floating point. A common choice for floating-point data. Float Single-precision floating point. Often used when interfacing with C. Int Fixed-precision signed integer; minimum range [-2^29..2^29-1]. Commonly used. Int8 8-bit signed integer Int16 16-bit signed integer Int32 32-bit signed integer Int64 64-bit signed integer Integer Arbitrary-precision signed integer; range limited only by machine resources. Commonly used. Rational Arbitrary-precision rational numbers. Stored as a ratio of two Integers. Word Fixed-precision unsigned integer; storage size same as Int Word8 8-bit unsigned integer Word16 16-bit unsigned integer Word32 32-bit unsigned integer Word64 64-bit unsigned integer


… 표…

숫자형 타입간의 변환은 흔하게 필요하다. 테이블 6.2 에 변환에 쓰일 많은 함수가 소개되어 있다.

하지만 모든 타입들간에 변환이 가능한 것이 아니라, 변환 가능여부가 테이블 6.3 에 나와있다.

타입 변환을 위해 필요한 함수표는 테이블 6.4 를 살펴볼 것.


Equality, Ordering, and Comparisons


+ 같은 연산자가 모든 종류의 숫자에 적용 가능할 정도로 광범위한 연산자이지만,

그보다 더 광범위하게 적용되는 연산자는 동일성 테스트에서 봤던 == 와 /= 이다. (Eq 클래스)

물론 >=, ⇐ 같은 비교연산자도 있다. (Ord 타입클래스)


– 팁 때때로 Ord 에서 순서가 임의적일 수 있다. 예를들어, Maybe 타입의 경우 Nothing 이 Just x 앞에 온다. 다소 임의적인 순서인데, * 따로 비교함수가 없으면 constructor 정의순서가 순서가된다.



자동 유도 Automatic Derivation


요약 : 타입클래스 instance 를 자동으로 유도해준다. deriving(T) 를 사용한다.

그런데 새로 만든 타입에 동일성테스트를 추가하려면, 매번 Eq instance 를 구현해야 하나??-_-!!!!

다행히도, 간단한 데이터 타입에서, 하스켈 컴파일러는 자동적으로 Read, Show, Bounded, Enum, Eq, Ord 의 instance 를 찾을 수 있다.

-- file: ch06/colorderived.hs
data Color = Red | Green | Blue
     deriving (Read, Show, Eq, Ord)

– Which types can be automatically derived? 하스켈 standard 는 위의 특수한 타입클래스의 instance 를 자동으로 유추하는 컴파일러 수준을 요구한다. 이런 자동 유추는 다른 타입클래스에서는 유효하지 않다.


ghci> show Red
"Red"

ghci> (read "Red")::Color
Red

ghci> (read "[Red,Red,Blue]")::[Color]
[Red,Red,Blue]

ghci> (read "[Red, Red, Blue]")::[Color]
[Red,Red,Blue]

ghci> Red == Red
True

ghci> Red == Blue
False

ghci> Data.List.sort [Blue,Green,Blue,Red]
[Red,Green,Blue,Blue]

ghci> Red < Blue
True

sort 도 된다!?! Color 의 정렬은 contructor 정의 순서에 따른 것이다.

자동 유도는 항상 가능하지는 않다.

data MyType = MyType (Int -> Bool)

위 같은 타입을 정의할때, 컴파일러는 Show instance 를 자동유도할 수 없다. 이유는 위 함수를 어떻게 show 를 할 지 알 수 없기 때문에 에러가 난다.

-- file: ch06/AutomaticDerivation.hs
data CannotShow = CannotShow
                -- deriving (Show)  -- 이 값의 존재 유무에 따라 컴파일 에러 여부가 결정된다.

-- will not compile, since CannotShow is not an instance of Show
data CannotDeriveShow = CannotDeriveShow CannotShow
                        deriving (Show)

data OK = OK

instance Show OK where
    show _ = "OK"

data ThisWorks = ThisWorks OK
                 deriving (Show)

CannotShow 타입에서 deriving(Show) 를 빼고 정의하면, 밑의 CannotDeriveShow 타입에서 deriving(Show) 를 사용할 수가 없다.

구성요소가 모두 Show 의 instance 구현이 되어있어야 deriving(Show) 를 적용할 수 있기 때문이다.

CannotShow 타입에 deriving(Show) 를 적용하던지 CannotDeriveShow 타입의 Show instance 를 직접 구현해야 할 것이다.


타입클래스를 사용해서 JSON 을 다뤄보자


(* 챕터 5에서 나온 SimpleJSON 모듈을 그대로 사용한다)

아래는 순수 Json 표현법

{
  "query": "awkward squad haskell",
  "estimatedCount": 3920,
  "moreResults": true,
  "results":
  [{
    "title": "Simon Peyton Jones: papers",
    "snippet": "Tackling the awkward squad: monadic input/output ...",
    "url": "http://research.microsoft.com/~simonpj/papers/marktoberdorf/",
   },
   {
    "title": "Haskell for C Programmers | Lambda the Ultimate",
    "snippet": "... the best job of all the tutorials I've read ...",
    "url": "http://lambda-the-ultimate.org/node/724",
   }]
}

아래는 동일한 Json 값에 대한 하스켈 JValue 버전 형태이다. (*JValue 는 챕터 5장에 소개되었었다)

-- file: ch05/SimpleResult.hs
import SimpleJSON

result :: JValue
result = JObject [
  ("query", JString "awkward squad haskell"),
  ("estimatedCount", JNumber 3920),
  ("moreResults", JBool True),
  ("results", JArray [
     JObject [
      ("title", JString "Simon Peyton Jones: papers"),
      ("snippet", JString "Tackling the awkward ..."),
      ("url", JString "http://.../marktoberdorf/")
     ]])
  ]

하스켈은 태생적으로 <여러타입의 값을 가지는 리스트>를 지원하지 않기 때문에,

직접적으로 JSON 객체를 표현할 수 없다.

대신에, JValue 라는 constructor 로 감싸야했다.

이는 유연성에 문제가 있는데

값의 타입이 변할경우, 거기에 맞춰서 JValue constructor 도 변경되어야 하기 때문이다.

타입클래스를 사용하면

'임의의 타입' 을 인자로 받아서 'JValue' 로 변경해주는 함수와

'JValue' 를 인자로 받아서 '임의의 타입' 으로 변환해주는 함수를 만들 수 있어서,

데이터 값의 타입이 변할때마다 수정해줘야하는 수고로움을 없앨 수 있다.

{-# LANGUAGE FlexibleInstances #-} 
module JSONClass where

import SimpleJSON

type JSONError = String

doubleToJValue :: (Double -> a) -> JValue -> Either JSONError a
doubleToJValue f (JNumber v) = Right (f v)
doubleToJValue _ _ = Left "not a JSON number"

class JSON a where
    toJValue :: a -> JValue
    fromJValue :: JValue -> Either JSONError a

instance JSON JValue where
    toJValue = id
    fromJValue = Right

instance JSON String where
    toJValue               = JString
    fromJValue (JString s) = Right s
    fromJValue _           = Left "not a JSON string"
    
instance JSON Int where
    toJValue = JNumber . realToFrac
    fromJValue = doubleToJValue round

instance JSON Integer where
    toJValue = JNumber . realToFrac
    fromJValue = doubleToJValue round

instance JSON Double where
    toJValue = JNumber
    fromJValue = doubleToJValue id

instance JSON Bool where
  fromJValue (JBool b) = Right b
  fromJValue _ = Left "not a JSON boolean"

JSON 타입클래스를 만들었고, 'toJValue' 와 'fromJValue' 함수를 instance 구현을 했다.

이제 JNumber 생성자를 통해서 값을 감쌀 필요 없이 toJValue 함수를 적용하면 된다. 우리가 해당 값을 바꾸면 컴파일러가 알아서 맞는 toJValue 구현을 선택해서 사용해 줄것이다.

*JSONClass> toJValue 1
JNumber 1.0
*JSONClass> toJValue True
JBool True
*JSONClass> toJValue "abc"
JString "abc"
*JSONClass> 

잘 동작한다.

(HELP! fromJValue (JNumber 1) 등, fromJValue 함수를 실행하면 에러가 발생하네요;; -_- 좀 찾아보고 있는데… 일단 알게 될때까지 여기에 적고 패스합니다)

추가로 언급해야할 사항.

1. FlexibleInstances 언어확장

{-# LANGUAGE FlexibleInstances #-} 

의 경우, string 은 [Char] 타입의 synonym 이고,

원래 하스켈 스펙상 instance 구현시 synonym 타입은 사용할 수 없어서 에러를 발생시킨다.

소스 첫줄에 FlexibleInstances 언어확장 주석을 넣으면,

synonym 타입으로 instance 구현을 허락해준다.

2. Either 타입은 … 뒤쪽에 설명이 나온다.

여기서 간단히 설명하자면, 에러가 발생했을때 Maybe 의 nothing 과 유사하지만, 추가 에러정보를 가지고 있을 수 있는 타입이다.


More helpful errors


위 코드에서 fromJValue 함수의 리턴 타입은 Either 타입이다. Maybe 타입과 유사하게, 실패할 것을 명시하는데 사용한다.

Maybe 가 이 목적에 적합한데, 실패가 발생했을 시, 아무 정보도 나타내지 않는데 반해(문자그대로 Nothing 이다.)

Either 타입은 이것과 유사한 구조지만, Nothing 대신에 “something bad happened” constructor 을 나타내는 Left 가 있다.

-- file: ch06/DataEither.hs
data Maybe a = Nothing
             | Just a
               deriving (Eq, Ord, Read, Show)

data Either a b = Left a
                | Right b
                  deriving (Eq, Ord, Read, Show)

종종, a 의 타입은 String 이며, 뭔가 문제가 생겻을때 이를 표현하는데 유용하다. Either 가 실제 어떻게 작동하는지 보려면, 아래 코드를 보자.

-- file: ch06/JSONClass.hs
instance JSON Bool where
    toJValue = JBool
    fromJValue (JBool b) = Right b
    fromJValue _ = Left "not a JSON boolean"

Making an instance with a type synonym


사실 위에서 JSON 타입클래스 작성 코드에서 보았던 아래 코드는 Haskell 98 standard 에서는 instance 작성은 불가능합니다.

-- file: ch06/JSONClass.hs
instance JSON String where
    toJValue               = JString

    fromJValue (JString s) = Right s
    fromJValue _           = Left "not a JSON string"

위 코드를 첫줄에 {-# LANGUAGE TypeSynonymInstances #-} 없이 실행하게 되면,

Prelude> :load JSONClass
[1 of 1] Compiling Main             ( JSONtest.hs, interpreted )

JSONtest.hs:22:10:
    Illegal instance declaration for `JSON String'
      (All instance types must be of the form (T t1 ... tn)
       where T is not a synonym.
       Use -XTypeSynonymInstances if you want to disable this.)
    In the instance declaration for `JSON String'
Failed, modules loaded: none.

와 같은 에러가 발생합니다.

String 은 오리지널 형태의 타입이라기 보다는 [Char] 에 대한 synonym 입니다.

(type String = [Char] 로 만들어 졌다는 의미)

Haskell 98 의 규칙에 따르면, instance 구현시 타입 자리에 synonym 은 올 수 없습니다.

GHC 기본적으로 Haskell 98을 따르지만, 이 특수한 제약을 낮출 수도 있다. 소스파일 가장 첫째줄에 다음과 같이 적으면 된다.

-- file: ch06/JSONClass.hs
{-# LANGUAGE TypeSynonymInstances #-}

-- 책에서는 아래와 같이 나와있지만, 위가 맞는 표현임
{-# LANGUAGE FlexibleInstances #-}

이 코멘트는 컴파일러에게 직접적으로 전달하는 pragma 로써, 언어 확장을 가능하게 해준다.

TypeSynonymInstances 언어 확장은 위의 코드를 허용하게 한다. 다른 언어 확장에 대해서도 나중에 만나게 될것이다.


Living in an open world


이렇게 타입클래스를 통해 함수에 '임의 타입의 인자' 를 사용할 수 있게 합니다.

이는 현재까지 구현되 있는 타입 외에도 미래에 추가될 타입들도

instance 구현만 하면 언제든지 '임의 타입' 안에 들어갈 수 있습니다.

또 instance 구현은 모듈 위치 제약 없이 어느곳이든지 새 instance 를 추가할 수 있습니다.

타입클래스 시스템의 이런 특징은 “open world assumption” 이라고 불린다.

“closed world” 는 이와 반대로 여기까지 구현이 마지막입니다 라는 의미임.

* 추가로 undefined 에 대해서 알아봅시다.

이전 챕터에서도 언급되었었는데, 단순히 클래스나 함수 구조를 잡을때 사용되는 키워드입니다.

컴파일러에서는 문제가 없지만, 실제 평가하려고 하면 런타임 에러를 발생시키는 키워드입니다.

-- file: ch06/BrokenClass.hs
instance (JSON a) => JSON [a] where
    toJValue = undefined
    fromJValue = undefined

instance (JSON a) => JSON [(String, a)] where
    toJValue = undefined
    fromJValue = undefined

지금까지 instance 구현 형태와는 조금 다른 부분이 나왔는데,

'⇒' 화살표 왼쪽 부분은 일종의 제약 조건입니다.

(JSON a) => JSON [a]

에서 왼쪽 제약조건 의미는 “a 가 JSON 타입클래스에 속할때”(= instance 구현되있는 타입일때) 입니다.

오른쪽에 JSON [a] 의 의미는 [a] 타입에 JSON 타입클래스를 적용하면,

… 다시 말하자면, JSON 타입클래스에 속한 임의의 타입 a 에 대하여, [a] 에 대해 JSON 타입클래스의 instance 를 구현하겠다는 말입니다.

Int, String, [Int], [(String, Int)] 등등 어떤 형태이든 각각이 독립된 타입 형태를 의미합니다.


When do overlapping instances cause problems?


instance 구현 오버랩핑은 언제 문제를 발생시킬까?

-- file: ch06/Overlap.hs
class Borked a where
    bork :: a -> String

instance Borked Int where
    bork = show

instance Borked (Int, Int) where
    bork (a, b) = bork a ++ ", " ++ bork b

instance (Borked a, Borked b) => Borked (a, b) where
    bork (a, b) = ">>" ++ bork a ++ " " ++ bork b ++ "<<"

위 코드는 소스파일에 작성하고 ghci 로 읽어드릴때, 처음에는 문제가 없어보인다.

ghci> :load BrokenClass
[1 of 2] Compiling SimpleJSON       ( ../ch05/SimpleJSON.hs, interpreted )
[2 of 2] Compiling BrokenClass      ( BrokenClass.hs, interpreted )
Ok, modules loaded: SimpleJSON, BrokenClass.

그러나, Int 쌍에 대한 instance 를 사용하는 순간 문제가 발생한다.

ghci> toJValue [("foo","bar")]

<interactive>:1:0:
    Overlapping instances for JSON [([Char], [Char])]
      arising from a use of `toJValue' at <interactive>:1:0-23
    Matching instances:
      instance (JSON a) => JSON [a]
        -- Defined at BrokenClass.hs:(44,0)-(46,25)
      instance (JSON a) => JSON [(String, a)]
        -- Defined at BrokenClass.hs:(50,0)-(52,25)
    In the expression: toJValue [("foo", "bar")]
    In the definition of `it': it = toJValue [("foo", "bar")]

위의 overlapping instances 문제는 하스켈이 가진 open world assumption 에 따른 결과이다.

다시 맨처음 나왔던 Borked 타입클래스의 소스를 살펴보자.

-- file: ch06/Overlap.hs
class Borked a where
    bork :: a -> String

instance Borked Int where
    bork = show

instance Borked (Int, Int) where
    bork (a, b) = bork a ++ ", " ++ bork b

instance (Borked a, Borked b) => Borked (a, b) where
    bork (a, b) = ">>" ++ bork a ++ " " ++ bork b ++ "<<"

페어 쌍에 대한 instance 구현은 두개가 있다.

(Int, Int) 와 Borked 타입인 a,b 에 대하여 (a, b) 이렇게 두개이다.

문제는 첫번째 instance 인 Borked Int 때문이다.

정수쌍 타입에 대해서 두번째 instance (Int, Int) 에도 들어맞고,

Int 는 Borked 타입클래스에 속해있으니,

Borked 타입인 a,b 에 대하여 (a, b) 쌍 이라는 정의에도 성립한다.

결과적으로 컴파일러가 두 개의 instance 중 어느것에 적용해야 될지 알 수 없어서 에러를 냈다.


– When do overlapping instances matter? 언제 instance 오버랩핑이 중요할까? 초기에 언급했듯이, 여러 모듈에 instance 들이 흩어져 있을 수 있다. GHC는 오버랩된 instance 때문에 문제가 나는게 아니라, 단지, 타입클래스의 영향을 받는 메서드를 시도하려고 할때, 어느 instance 를 사용할지 선택을 강요할때 발생한다…???? (→코드 자체에서 컴파일 에러가 나는 것이 아니라, 런타임시 애매한 상황에서 선택을 강요받을때 에러가 난다는 말인듯; )



Relaxing some restrictions on typeclasses


보통, 다형성 타입을 위한 특수한 버전에 타입클래스 instance 를 작성할 수 없다. [Char] 타입은 다형성 타입 [a] 를 Char 타입으로 상세화시킨 것이다.

이와같이 [Char] 를 타입클래스의 instance 에 선언되는 것은 금지되어있다. 이는 매우 불편하다. 문자열은 실제 코드 작성시 곳곳에 존재하기 때문이다..??

TypeSynonymInstances 언어확장은 이런 제약을 제거하며, 그런 instance 작성을 허용한다.

GHC 가 지원하는 또다른 언어 확장은 OverlappingInstances 이며, 이는 instance 오버랩 문제를 해결한다. 여러개의 오버랩된 instance 존재시, 컴파일러가 가장 특유의 것을 선택하도록 한다.

TypeSynonymInstances 언어확장을 자주 보게된다.

-}

-- file: ch06/SimpleClass.hs
{-# LANGUAGE TypeSynonymInstances, OverlappingInstances #-}

import Data.List

class Foo a where
    foo :: a -> String

instance Foo a => Foo [a] where
    foo = concat . intersperse ", " . map foo

instance Foo Char where
    foo c = [c]

instance Foo String where
    foo = id

소스 첫줄에 언어확장 주석을 첨부하였기 때문에,

[a] 타입의 instance 가 구현되었고,

밑에 좀 더 구체적인 [Char](==String) 타입이 오버랩 되서 instance 구현이 되었지만, 에러는 안난다.

foo 에 String 을 적용하면, 컴파일러가 String specific한 (더 구체적이고 잘맞는) 구현을 사용하게 될 것이다.

GHC 는 같은정도의 specific instance 를 하나이상 발견시 코드를 거부(?)할 것이다.


– When to use the OverlappingInstances extension 언제 이 언어확장을 사용할까. 중요한 점은 이것이다

: GHC 가 OverlappingInstances 를 instance 선언에 영향을 주는데 사용한다. instance 를 사용하는 위치가 아니다!. 다시말하자면, 오버랩을 허용하게 instance 를 정의하려면, “정의하는 위치”에 이 언어확장을 넣어야한다. 모듈이 컴파일 될때, GHC는 이를통해 “다른 instance 로 오버랩 될 수 잇음”을 기억하게 된다.

한번 이 모듈이 임포트 되고 instance 가 사용되면, 임포트하는 모듈에서는 OverlappingInstances 언어 확장은 더이상 필요가 없다. : GHC 가 이미 “오버랩 가능해” 라고 정의할때 마크해두었기 때문이다.

이러한 행동패턴은 라이브러리 작성시 유용하다. : 오버랩가능한 instance 작성을 선택할 수 있다. 그러나, 사용자가 특정 언어 확장을 사용할 필요는 없다.



How does show work for strings?


show 함수는 string에 어떻게 작동할까?

“abcd” 는 사실 ['a','b','c','d'] 인데, 출력은 [,] 형태가 아니라 “” 형태일까?

OverlappingInstances, TypeSynonymInstances 언어 확장은 GHC 에만 있는 것이며, Haskell 98 에는 본래 정의에 따라 존재하지 않는다.

그러나, 친근한 Show 타입클래스의 경우에는 Char 리스트를 Int 리스트와 좀 다르게 다룬다. 이는 매우 clever, simple, trick 적인 방법으로 가능해진다.

Show 클래스는 show, showList 함수가 있다. showList 의 디폴터 구현에서 리스트는 [ ] 와 , 로 구성된다.

[a] 에 대한 Show instance 는 showList 를 사용해서 구현되었다. Char 에 대한 Show instance 는 특수하게 “ 와 이스케이프 문자를 사용한다.

결과적으로, [Char] 값에 show 를 적용하려면, showList 구현이 선택되서 바르게 ” 으로 감싸진 문자열이 만들어진다.

적어도 가끔은, 이렇게 우회하는 방법으로 OverlappingInstances 확장 필요없이도 가능하다.


How to give a type a new identity


http://www.haskell.org/haskellwiki/Newtype 설명을 참조하였음.

data 와 newtype 은 거의 똑같다. newtype 자리에 data 를 넣어도 컴파일되고, 잘 작동한다. 차이점은, data 자리에 newtype 을 넣으려면, 오직 1개의 생성자만 있어야 한다. …

-- file: ch06/Newtype.hs
data DataInt = D Int
    deriving (Eq, Ord, Show)

class TypeClass ...

type String = [Char] ...

newtype NewtypeInt = N Int
    deriving (Eq, Ord, Show)

– The type and newtype keywords 비록 이름이 비슷하지만, type 과 newtype 키워드는 다른 목적을 가진다.

type 키워드는 일종의 닉네임같은 참조를 제공한다.

이와 반대로, newtype 은 기본 타입을 숨긴다?!. UniqueID 타입을 보자.

-- file: ch06/Newtype.hs
newtype UniqueID = UniqueID Int
    deriving (Eq)

컴파일러는 Int 와 UniqueID 를 다른 타입으로 본다. 유저로서는 그것이 Int 로 구현됬음을 알 수 없다.


newtype 을 사용할때, 어떤 타입클래스 instance 를 노출할지를 선택해야한다.

Int 컴포넌트를 사용했지만, deriving (Eq, Ord, Show) 를 했기 때문에

동일성, 정렬, 출력은 Int 의 instance 에서 유도하지만,

Int 의 Num or Integer instance 구현은 노출하지 않았기 때문에 해당되지 않는다.

ghci> N 1 < N 2
True

ghci> N 313 + N 37

<interactive>:1:0:
    No instance for (Num NewtypeInt)
      arising from a use of `+' at <interactive>:1:0-11
    Possible fix: add an instance declaration for (Num NewtypeInt)
    In the expression: N 313 + N 37
    In the definition of `it': it = N 313 + N 37

기존타입에서 일부 성질만을 가져와서 기존과는 다른 identity 를 제공한다고 볼 수 있다.


Differences between data and newtype declarations


data 와 newtype 의 차이점

newtype 은 기존 타입에 new identity 를 주고 싶을때 사용한다.

data 보다 제약이 많다.

자세히 말하자면, 오직 1개의 생성자만을 가지며, 1개의 필드만을 가진다.

그보다 더 중요한 차이가 있다.

data 타입은 런타임 비용을 book-keeping 한다.

예를들어, 어느 생성자가 값을 만드는데 사용했는지 추적한다.

newtype 값은 하나의 생성자만 가지고 있어서 위같은 추적이 불필요하다.

결론적으로 런타임에서 공간, 시간 효율성이 증가한다.

newtype 의 생성자는 오직 컴파일 시간에서만 사용되고,

런타임에는 존재조차 안하며,

undefined 에 관한 패턴매칭시 newtype 과 data 의 결과가 다르다.

ghci> undefined
*** Exception: Prelude.undefined

ghci> case D undefined of D _ -> 1
1

패턴미칭이 생성자를 통해서 이루어짐으로, undefined 는 평가되지 않은채 남아 에러가 안난다. 만약 보호되지 않은 undefined 를 바로 사용하면 에러가 발생한다.

ghci> case undefined of D _ -> 1
*** Exception: Prelude.undefined

하지만, newtype 의 경우,

ghci> case N undefined of N _ -> 1
1
ghci> case undefined of N _ -> 1
1

에러가 안났다!!! 가장 중요한 차이점은 런타임시에는 N 생성자는 존재 하지 않는다.

실재로 N _ → 패턴매칭은 _ → 와 사실상 같다.

(이부분은 좀 더… 공부해야 될 거 같네요 -_-; 이해가 잘… newtype 으로 선언한 타입의 경우,

실제로 생성자 체크를 런타임에도 하지 않기 때문에 더 효율적이라고 하는거 같은데…흠)


JSON typeclasses without overlapping instances


마지막 소챕터… 준비중 ㅜㅜ

study/haskell/ch6.txt · Last modified: 2019/02/04 14:26 (external edit)