21
Haskell Study 6. type & type class 2

Haskell study 6

Embed Size (px)

Citation preview

Page 1: Haskell study 6

Haskell Study

6. type & type class 2

Page 2: Haskell study 6

Algebraic Data type

Haskell에서 새로운 데이터 타입을 정의할 때는 data 키워드를 이용합니다. 표준에서 Bool 데이터

타입은 아래와 같이 선언되어 있습니다.

data Bool = False | True

data 다음 부분이 타입의 이름이고, = 이후의 부분이 해당 타입의 생성자(값 생성자 - value

constructor)를 가리킵니다. | (or)을 이용해 여러 개의 생성자를 만들 수 있습니다. 위의 data

구문은 'Bool 타입은 False 또는 True라는 값 생성자를 가진다' 라는 의미로 볼 수 있습니다. 타입

이름, 값 생성자는 반드시 대문자로 시작해야합니다.

Page 3: Haskell study 6

Algebraic Data type

Haskell에서 어떤 도형에 대한 정보를 나타내고 싶다고 해 봅시다. 예를 들어 원은 중점의 좌표 및

반지름, 사각형은 좌상단과 우하단의 좌표 등으로 표현할 수 있습니다. 이런걸 튜플을 이용해 표현할

수도 있지만, 튜플은 의미를 명확히 파악하기가 쉽지 않기 때문에 새로운 타입을 정의하는 게 좋겠죠.

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

위의 예제와 같이 각 값 생성자들은 필드(Field)를 가질 수 있습니다. Circle 값 생성자는 Float형의

값 3가지를 필드로 갖게 되고(첫 두개는 중점, 나머지 하나는 반지름), Rectangle 값 생성자는 Float

형의 값 4가지를 필드로 갖게 되는 거죠(첫 두개는 좌상단, 나머지 두개는 우하단 좌표).

Page 4: Haskell study 6

Algebraic Data type

값 생성자는 사실 필드의 값이 주어졌을 때 해당 타입의 값을 반환하는 함수라고 볼 수 있습니다.

Prelude> :t CircleCircle :: Float -> Float -> Float -> ShapePrelude :t RectangleRectangle :: Float -> Float -> Float -> Float -> Shape

그리고 값 생성자는 아래와 같이 해당 타입의 값에 대한 함수의 패턴 매칭에서 사용할 수 있습니다.

surface :: Shape -> Floatsurface (Circle _ _ r) = pi * r ^ 2surface (Rectangle x1 y1 x2 y2) = abs $ (x2 - x1) * (y2 - y1)

Page 5: Haskell study 6

Algebraic Data type

Bool 타입의 값 생성자 True, False처럼 값 생성자는 필드 값을 가지고 있을 수도 있고 아닐 수도

있습니다. 그리고, 앞에서 봤듯이 값 생성자 역시 함수이기 때문에 커링이 되고, 따라서 부분 적용의

이점을 값 생성자에도 그대로 적용시킬 수 있습니다.

{-- 맨 끝에 deriving Show를 붙이면 Show 타입 클래스를 자동으로 상속받게 됩니다. 조금 나중에

다루니 여기서는 일단 "내가 직접 만든 데이터 타입을 결과 창에 띄우려면 deriving Show를 써줘야

한다" 정도로만 이해해주시면 되겠습니다. --}

data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving Show

Prelude> map (Circle 10 20) [4,5,6,6][Circle 10 20 4, Circle 10 20 5, Circle 10 20 6, Circle 10 20 6]

Page 6: Haskell study 6

Record Syntax

어떤 사람에 대한 정보를 타입으로 나타내고 싶다고 합시다.

data Person = Person String String Int String deriving (Show)

첫 두 개는 각각 이름과 성, 세 번째는 나이, 네 번째는 주소라고 합시다. 이제 이 데이터 타입으로부터

각 필드의 값을 가져오려면 아래와 같은 함수들을 만들어야합니다.

firstname (Person firstname _ _ _) = firstnamelastname (Person _ lastname _ _) = lastnameage (Person _ _ age _) = ageaddress (Person _ _ _ address) = address

Page 7: Haskell study 6

Record Syntax

척 봐도 알겠지만 앞에서처럼 각 필드의 값을 가져오기 위해 일일히 함수를 만들어줘야한다는 것은

굉장히 불편하고 귀찮은 일입니다. Haskell에서는 이런 상황을 위해 Record Syntax라는 문법을

제공합니다.

data Person = Person { firstname :: String , lastname :: String , age :: Int , address :: String } deriving (Show)

Prelude> let nam = Person "nam" "hyeonuk" 21 "blahblah"Prelude> age nam21

Page 8: Haskell study 6

Record Syntax

Record syntax는 앞에서와 같이 값 생성자 뒤에 중괄호 ({}), 그 내부에 각 필드의 이름과 타입

어노테이션(::)을 통한 해당 필드의 타입 명시로 이루어져있습니다. Record syntax로 생성된 타입은

각 필드의 값을 가져오는 함수가 자동으로 생성됩니다. Record syntax로 작성된 데이터 타입은 해당

타입의 값 생성자로 값을 만들 때에도 Record syntax 스타일을 사용할 수 있습니다. 이 방식으로 쓸

때에는 반드시 필드의 순서가 선언 순서와 일치할 필요는 없습니다.

data Circle = Circle { origin :: (Int, Int) , radius :: Int } deriving (Show)

Prelude> let c = Circle { radius = 5, origin = (0,0) }Prelude> origin c(0,0)

Page 9: Haskell study 6

Type parameter

C++의 템플릿과 비슷한 개념으로 Haskell에는 타입 생성자(Type constructor)라는 것이 있습니다.

타입 생성자는 타입을 매개 변수로 받아서 새로운 타입을 만들어냅니다. 표준에 있는 유용한 타입

생성자인 Maybe를 예로 들어봅시다.

data Maybe a = Nothing | Just a

여기서 a가 바로 타입 매겨변수입니다. Maybe 타입 생성자는 Nothing 값 생성자가 아니라면 임의의

타입 a에 대해 해당 타입의 값을 하나 필드로 들고 있는 값을 만들어내죠. Maybe Int, Maybe Char같은 것들이 하나의 타입이 되는 겁니다. Maybe Int 타입의 값으로 Just 3, Just 22

같은 게 있을 수 있고, Maybe Char 타입의 값으로 Just 'a', Just 'c'같은 값들이 있을 수

있겠죠.

Page 10: Haskell study 6

Type parameter

여태껏 써왔던 리스트(list)역시 일종의 타입 매개변수를 받습니다. 여태껏 우리는 임의의 타입에 대한

리스트를 써왔죠. 실제로는 이 리스트 역시 임의의 타입 a에 대해 [a] 타입을 생성하는 타입 생성자를

기반으로 동작하고 있는 것입니다.

타입 매개변수는 내부에 들고 있는 필드의 값이 모든 종류의 값에 대해 일반적으로 동작할 필요가

있을 경우에 사용하면 굉장히 좋습니다.

Page 11: Haskell study 6

Type synonyms

어떤 타입에 대해 별명을 붙여주고 싶을 때 type 키워드를 사용할 수 있습니다. C언어의 typedef,

C++ 11의 using 키워드와 동일한 역할이라고 생각하시면 됩니다. type 키워드는 내부적으로는

완전히 동일하나 이름만 서로 다른 데이터 타입을 만들어냅니다(서로 호환도 됩니다). type 키워드는

코드의 가독성을 위해 사용합니다. 대표적인 예로 String과 [Char]을 들 수 있겠네요.

type Point = (Int, Int)

distance :: Point -> Point -> Intdistance (x1,y1) (x2,y2) = sqrt $ (x2-x1)^2 + (y2-y1)^2

Page 12: Haskell study 6

Derived instances

Haskell의 기본 타입클래스들은 deriving 키워드를 이용해 별도의 정의 없이 해당 타입 클래스의

기본 구현체를 상속받을 수 있습니다. 어떤 타입이 해당 타입 클래스에 정의된 동작을 지원할 때 해당

타입 클래스의 인스턴스(instance)라고 합니다. deriving 키워드를 이용하면 해당 타입 클래스의

동작을 기본적으로 구현하게 됩니다. 앞에서도 봤듯이 deriving Show 를 이용하면 컴파일러가

자동으로 해당 타입이 Show 타입 클래스의 행동을 구현하게 만들어 주죠. Show외에도 Eq, Ord,

Enum, Bounded, Show, Read 등에 대해 deriving 키워드를 이용할 수 있습니다.

data Person = Person { firstname :: String , lastname :: String , age :: Int } deriving (Show, Eq)

Prelude> Person "nam" "hyeonuk" 21 == Person "nam" "hyeonuk" 21True

Page 13: Haskell study 6

Recursive Data

데이터 타입은 재귀적인 구조를 가질 수 있습니다. list를 직접 구현해봅시다.

data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)

Prelude> 5 `Cons` EmptyCons 5 EmptyPrelude> 4 `Cons` 5 `Cons` EmptyCons 4 (Cons 5 Empty)

위의 List 데이터 타입은 Empty(빈 리스트 [])와 Cons(:)라는 두 개의 값 생성자를 가집니다.

그리고 Cons 생성자는 다시 자신의 필드로 List a를 가집니다. 재귀적인 구조가 되는 거죠. 이 재귀적

구조를 이용해서 위와 같이 리스트를 구현할 수 있습니다.

Page 14: Haskell study 6

Recursive Data

동일한 방식으로 바이너리 트리 역시 구현할 수 있습니다.

data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

위 트리구조에 삽입 / 삭제 함수를 적절히 원하는 구조로 작성하면 자신의 필요에 맞게 간단한 Binary

Search Tree 등을 구현할 수 있습니다.

Page 15: Haskell study 6

typeclass

이번엔 직접 타입 클래스를 한 번 만들어봅시다. 우선 예제로 표준에 있는 Eq 타입 클래스가 어떻게

구현되어있는지 먼저 살펴보죠.

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

새로운 타입 클래스를 정의하기 위해 class 키워드를 사용합니다. class 뒤에 타입 클래스의 이름과

해당 타입 클래스에 속하는 타입을 지칭하기 위한 타입 매개변수, 그리고 where 이후에 해당 타입

클래스에 속하는 타입들이 수행할 수 있어야만 하는 동작들을 정의합니다.

Page 16: Haskell study 6

typeclass

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

타입 클래스 정의의 맨 아래쪽을 보면 x == y와 x /= y가 서로 재귀적으로 정의되어 있는 것을 볼

수 있습니다. 이 재귀적 정의에 의해 == 또는 /= 둘 중 하나만 정의하면 나머지 하나도 그 정의에

의해 자동으로 정의가 됩니다. 이 덕분에 Eq 타입 클래스에 속하는 타입이 ==과 /= 동작을 어떻게

수행해야할 지 정의할 때 둘 모두를 정의할 필요 없이, 정의하기 편한 것 하나만 정의하면 되는

편의성을 얻을 수 있죠.

Page 17: Haskell study 6

typeclass

이제 특정 타입이 어떤 타입 클래스의 인스턴스가 되려면 어떻게 해야하는지 정의하는 방법을

살펴봅시다.

data TrafficLight = Red | Yellow | Green

신호등 색깔을 나타낸 데이터 타입입니다. 이 타입이 Eq 타입 클래스에 속하게 만들려면 아래와 같이

해야합니다.

instance Eq TrafficLight where Red == Red = True Green == Green = True Yellow == Yellow = True _ == _ = False

Page 18: Haskell study 6

typeclass

instance Eq TrafficLight where Red == Red = True Green == Green = True Yellow == Yellow = True _ == _ = False

타입 클래스의 인스턴스를 만들 때는 위와 같이 instance 키워드를 이용합니다. 그리고 원래

타입 매개변수 a로 표현했던 부분에 실제로 인스턴스로 만들고자 하는 타입을 적어주고, where

뒤에 정의해야하는 함수들의 동작을 정의해줍니다. 이 경우 TrafficLight에 대한 == 함수만을

정의해주었고, /= 함수는 재귀적 정의 ( not ( x == y ) )에 의해 자동으로 정의가 됩니다.

Page 19: Haskell study 6

typeclass

어떤 타입 클래스의 sub typeclass도 만들 수 있습니다. 예를 들어, Ord 타입 클래스는 아래와 같이

정의되어 있습니다.

class (Eq a) => Ord a where compare :: a -> a -> Ording ... -- 자세한 구현은 생략

위와 같이 이전에 타입 클래스 제약조건을 표기할 때 썼던 =>을 이용해 sub typeclass를 만들 수

있습니다. 위 선언의 의미는 Ord 타입 클래스에 속하기 위해서는 반드시 Eq 타입 클래스에 먼저

속해야만 한다는 것입니다. 이런 제약은 타입 클래스의 동작을 정의하기 위해 다른 타입 클래스의

동작이 필요한 경우 사용할 수 있습니다.

Page 20: Haskell study 6

typeclass

class (Eq m) => Eq (Maybe m) where Just x == Just y = x == y Nothing == Nothing = True _ == _ = False

Maybe 타입의 Eq 타입 클래스 인스턴스 정의입니다. 두 Just 값 비교는 그 내부 값이 같은 지

아닌지로 판별하고 있는데, 이 때 == 함수를 쓰려면 Maybe의 타입 매개변수 역시 Eq 타입 클래스에

속해야만 합니다. 따라서 Eq m 이라는 제약조건을 걸어 Just x 와 Just y 의 비교에 x == y를 사용할

수 있게 만들어야합니다.

Page 21: Haskell study 6

연습 문제

•binarysearchtreeBST를 만들어 봅시다. 트리 구조를 저장하기 위한 새로운 타입과, 트리에 원소를 삽입하는 함수, 어떤

원소가 해당 트리에 존재하는지 검색하는 함수 등을 구현해봅시다. BST는 각 노드에 대해 자신의 왼쪽

자식은 모두 자신보다 값이 작아야하며, 자신의 오른쪽자식은 모두 자신보다 값이 큰 트리를 말합니다.

또한 각 노드의 값은 유일해야합니다(중복되는 값이 존재하지 않습니다).

추가적으로 좀 더 연습을 해보고 싶으신 분은 노드를 삭제하는 함수, 혹은 Red-Black Tree나 AVL

Tree같이 균형잡인 트리를 만드는 코드를 한 번 작성해보시는 것도 도움이 될 것 같습니다.