haskell study 12

22
Haskell Study 12. monoid

Upload: nam-hyeonuk

Post on 17-Jan-2017

443 views

Category:

Software


1 download

TRANSCRIPT

Haskell Study

12. monoid

Before Monoid..

Monoid에 대해 이야기하기 전에, 아래의 함수들을 먼저 살펴봅시다.

*(곱셈), +(덧셈), ++(리스트 연결)

이 함수들의 공통적인 특성에는 무엇이 있을까요? 일단 다음의 두 가지 공통적인 특성을 가지고

있습니다.

• 두 개의 인자를 받습니다.

• 인자와 리턴 값이 같은 타입을 가집니다.

이건 굉장히 직관적이고 간단한 특성이죠. 그리고 조금 더 복잡한 공통 특성들도 있습니다.

Before Monoid..

항등원(Identity)곱셈, 덧셈, 리스트 연결등의 연산자는 항등원(Identity)을 가집니다. 항등원이란 해당 연산자에

사용됐을 때 결과값이 다른 피 연산자와 동일하게 나오는 값을 말합니다. 예를 들어 곱셈의 항등원은 1

이죠.

1*3 == 3

5*1 == 5

1은 곱셈에 사용됐을 때 항상 다른 피연산자의 값이 그대로 결과값이 된다는 것을 알 수 있습니다.

마찬가지로 덧셈의 항등원은 0, 리스트 연결의 항등원은 [] 입니다.

[1,2,3] ++ [] = [1,2,3] , 26 + 0 = 26

[] ++ [4,5,6] = [4,5,6] , 0 + 31 = 31

Before Monoid..

결합성(associativity) 또다른 특성은 바로 결합성입니다. 셋 이상의 값에 대해 이 함수들을 연속해서 적용할 때, 적용하는

순서에 상관없이 결과값이 항상 일정한 것을 결합성(associativity)이라고 하죠.

(2*3)*4 == 2*(3*4)

(5+7)+9 == 5+(7+9)

([2,3] ++ [6,7,8]) ++ [9,10,11] == [2,3] ++ ([6,7,8] ++ [9,10,11])

앞에서 제시한 이런 모든 특성들을 만족하는 동작을 할 수 있는 타입들이 바로 Monoid입니다.

Monoid에 속한 타입에 대해서는 위와 같은 특성을 만족하는 함수를 사용할 수 있는 거죠.

Monoid

Monoid의 선언은 다음과 같습니다.

class Monoid m where

mempty :: m

mappend :: m -> m -> m

mconcat :: [m] -> m

mconcat = foldr mappend mempty

Monoid 타입 클래스는 Data.Monoid 모듈에 정의되어 있습니다. 함수를 하나씩 살펴봅시다. 우선

mempty 함수는 아까 앞에서 언급했던 항등원(identity)입니다. mempty 함수(인자를 받지 않기

때문에 사실상 값과 같죠)가 해당 타입에서 항등원 역할을 하는 값인 거죠.

Monoid

mappend 함수가 앞에서 이야기했던 함수들(+, *, ++ 등등)에 해당하는 함수입니다. 마지막은

mconcat 함수죠. 이 함수는 기본 정의가 있고(mconcat = foldr mappend mempty), 대부분의

경우 이 정의로 충분하기 때문에 크게 신경 쓸 필요는 없습니다. mconcat 함수는 값의 리스트에

연속해서 mappend 함수를 적용시켜 최종 하나의 결과 값을 도출하는 함수인 거죠. 그리고 monoid

의 함수들은 아래의 규칙을 만족해야합니다.

mempty `mappend` x = x (left identity)

y `mappend` mempty = y (right identity)

(x `mappend` y) `mappend` z = x `mappend` (y `mappend` z) (associativity)

이 세 가지 특성은 앞에서 이미 언급한 내용이죠. Monad 파트에서도 말했듯이 컴파일러가 이러한

특성까지 검사해주진 않기 때문에 Monoid를 만들 땐 주의해서 작성해야 합니다.

List Monoid

List는 Monoid에 속하는 타입입니다. List Monoid는 다음과 같이 정의되어 있습니다.

instance Monoid [a] where

mempty = []

mappend = (++)

임의 타입의 리스트에 대해 항등원 mempty는 빈 리스트이며, mappend는 ++ 연산자입니다.

앞에서 예제로 다뤘듯이 임의 타입의 리스트에 대해 ++ 연산자는 monoid의 특성을 모두 만족하는

함수이기 때문이죠. 이로부터 List Monoid에 대한 mconcat 함수는 그냥 concat과 같다는 것 역시

알 수 있습니다.

newtype

그렇다면 숫자 타입(Num에 속하는 타입들)은 어떤 Monoid일까요? 곱셈과 덧셈 모두 Monoid의

특성을 만족하기 때문에 둘 중 하나로 정할 수 없다는 문제가 생깁니다. 이를 위해 사용되는 키워드가

바로 newtype입니다. Monoid에 대한 이야기를 더 하기 전에 newtype에 대해 먼저 짚고 넘어가죠.

newtype 키워드는 이미 존재하는 타입을 기반으로 새로운 타입을 만들 때 사용합니다. 내부적으로는

이미 존재하는 타입과 완전히 동일하지만, 외부적으로는 호환이 되지 않게 만들어주는 거죠. 이는 타입

체크를 통한 함수의 잘못된 사용을 더 엄격하게 막아주는 역할을 합니다. 문법은 data를 사용할 때와

동일하지만, 필드로 단 하나의 타입만을 가질 수 있다는 차이점이 있습니다. 기존의 타입으로부터

새로운 타입을 만들어내는 것이기 때문이죠.

newtype Vector2D = Vector2D { getVector2D :: (Int, Int) }

newtype

newtype의 유용성을 확인하기 위해 아래의 예제 코드를 살펴봅시다.

type BookId = Int

type UserId = Int

getBookName :: BookId -> String

getBookName = getName . findBookById

위와 같은 코드가 있다고 합시다. BookId와 UserId는 모두 정수값이기 때문에 Int형으로 만들었고,

가독성을 위해 type 키워드로 서로 다른 2개의 이름을 만들었습니다. 그러나 이렇게 코드를 작성할

경우의 문제점은 getBookName의 인자로 UserId 타입의 값을 넘겨도 컴파일이 된다는 거죠. 이

경우 논리적으로 명백한 오류임에도 불구하고, 내부적으로 두 개의 값은 동일한 Int이므로 컴파일

시에 아무런 이상을 감지할 수 없다는 거죠.

newtype

newtype BookId = BookId Int

newtype UserId = UserId Int

getBookName :: BookId -> String

getBookName = getName . findBookById

반면 위와 같이 newtype을 이용할 경우 내부적으로는 동일한 Int지만 서로 완전히 다른 타입으로

취급되기 때문에 BookId와 UserId가 서로 호환되지 않게 됩니다. 좀 더 엄격한 타입 체크가

가능해지는 것이죠.

하지만 이건 그냥 data로 선언해도 되는 것 아니냐? 라는 생각이 들 수도 있습니다. data 키워드로

하나의 필드를 갖는 타입을 새로 선언해도 newtype과 같은 효과를 낼 수 있으니까요. 여기서

newtype의 장점은, data와는 다르게 런타임에 새로운 타입을 생성시키지는 않는다는 것입니다.

기존 타입을 그대로 사용하기 때문에 data보다 좀 더 효율이 좋은 코드를 생성하게 되는 거죠.

Monoid

다시 Monoid 이야기로 돌아와 봅시다. 곱셈과 덧셈 두 개의 Monoid를 위해 Haskell에서는

newtype 키워드를 이용해 만든 Product와 Sum이라는 두 개의 타입을 제공합니다. 이 두 타입은

내부적으로는 단순 숫자 타입과 완전 동일하지만, Monoid 타입클래스의 함수를 적용시켰을 때

하나는 곱셈, 하나는 덧셈으로 동작하게 되는 거죠. 이 두개의 타입은 Data.Monoid 모듈에 다음과

비슷한 형태로 정의되어 있습니다.

newtype Product a = Product { getProduct :: a }

deriving (Eq, Ord, Read, Show, Bounded)

newtype Sum a = Sum { getSum :: a }

deriving (Eq, Ord, Read, Show, Bounded)

Monoid

두 타입의 Monoid 타입 클래스에 대한 인스턴스는 아래와 같이 정의되어 있습니다.

instance Num a => Monoid (Product a) where

mempty = Product 1

Product x `mappend` Product y = Product (x * y)

instance Num a => Monoid (Sum a) where

mempty = Sum 0

Sum x `mappend` Sum y = Sum (x + y)

newtype 키워드를 이용해 Num 타입클래스에 속한 타입들에 대해 Sum으로서의 Monoid,

Product로서의 Monoid 둘 모두를 제공하고 있습니다. 같은 타입에 대해 두 개 이상의 타입 클래스

인스턴스가 필요할 때 이런 식으로 newtype 키워드를 유용하게 사용할 수 있죠.

Maybe Monoid

어떤 타입 a가 Monoid 타입 클래스에 속한다면 Maybe a 역시 Monoid 타입 클래스에 속합니다.

이는 아래와 같이 정의되어 있습니다.

instance Monoid a => Monoid (Maybe a) where

mempty = Nothing

Nothing `mappend` m = m

m `mappend` Nothing = m

Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)

Prelude> Nothing `mappend` Just "test"

Just "test"

Prelude> Just (Sum 3) `mappend` Just (Sum 4)

Just (Sum {getSum = 7})

Maybe Monoid

Maybe a타입이 모노이드이기 때문에 a타입이 Monoid일 경우 실패할 수 있는 연산을 연속적으로

수행할 때 굉장히 유용하게 사용될 수 있습니다. 어떤 실패할 수 있는 연산들의 결과값을 모두 합치는

연산이 필요하다면 mconcat 함수만 사용해도 바로 결과를 얻어낼 수 있죠.

하지만 Maybe의 내부에 속한 타입이 Monoid에 속하지 않는 경우는 어떻게 해야할까요? 이런

경우를 위해 First라는 타입이 정의되어 있습니다.

newtype First a = First { getFirst :: maybe a }

deriving (Eq, Ord, Read, Show)

위와 같이 First는 Maybe 타입을 newtype 키워드로 래핑(wrapping)한 타입이며, 이 타입의

모노이드는 양변이 모두 내부에 정당한 값을 가지고 있을 경우(Just value 인 경우) 이름과 같이 첫

번째 값(좌변값)을 결과값으로 취하게끔 동작합니다.

Maybe Monoid

instance Monoid (First a) where

mempty = First Nothing

First (Just x) `mappend` _ = First (Just x)

First Nothing `mappend` x = x

위의 정의에서 볼 수 있듯이, 좌변에 값이 있다면 (First (Just x)) 우변의 값은 버리고 좌변값만

취하며, 좌변의 값이 없다면(First Nothing) 무조건 우변의 값을 취하는 방식으로 동작하죠.

내부에 존재하는 값이 Monoid에 속하지 않기 때문에 이러한 방식으로 연산 결과값을 합치는

것입니다. First는 연산 결과값 중에 단 하나라도 정당한 결과(Just x)가 존재하는지 확인하고

싶을 때 유용하게 사용할 수 있습니다.

Prelude> getFirst . mconcat . map First $ [Nothing, Just 9, Just 10]

Just 9

Maybe Monoid

반대로 Last 타입도 있습니다. 이 타입 역시 하는 역할은 First와 동일하나, 좌변이 아니라 우변의 값을

결과값으로 취한다는 차이만 있습니다.

newtype Last a = Last { getLast :: maybe a }

deriving (Eq, Ord, Read, Show)

instance Monoid (Last a) where

mempty = Last Nothing

_ `mappend` First (Just x) = First (Just x)

x `mappend` First Nothing = x

Prelude> getLast . mconcat . map First $ [Nothing, Just 9, Just 10]

Just 10

연습문제

숫자 타입에 Product, Sum이라는 두 가지 모노이드가 존재하듯이 Bool 타입에 대해서도 두 가지

모노이드가 존재합니다. 그 두가지는 바로 Any 모노이드와 All 모노이드인데, 아마 이름에서 이 둘이

어떤 식으로 mappend를 하는 지 짐작할 수 있으실 겁니다. Any, All 타입을 newtype 키워드를

통해 정의하고 해당 타입의 Monoid 타입 클래스에 대한 인스턴스를 정의해보세요. 물론 이름이

겹칠테니 MyAny, MyAll 등의 이름으로 정의하시면 됩니다.

* 참고 : mappend 함수는 중위 함수로 사용되는 일이 많아 <>라는 mappend와 동일한 역할을 하는

중위 연산자가 존재합니다. 이 함수를 사용하면 코드를 좀 더 간결하게 쓸 수 있습니다.

Prelude> [1,2,3] `mappend` [4,5,6]

[1,2,3,4,5,6]

Prelude> [1,2,3] <> [4,5,6]

[1,2,3,4,5,6]

Foldable

Monoid를 유용하게 활용할 수 있는 타입 클래스로 Foldable이라는 타입 클래스가 있습니다.

Foldable은 접을 수 있는(fold할 수 있는) 타입들이 속하는 타입 클래스로, Data.Foldable 모듈에

정의되어 있죠. 이 Data.Foldable 모듈의 함수는 Prelude 모듈의 함수들과 겹치는게 많기 때문에

되도록이면 qualified import하는게 좋습니다(import qualified Data.Foldable as F 등).

Foldable 타입 클래스 역시 foldl, foldr 등등의 함수를 제공합니다. 하지만 Prelude에 있는 함수와

타입 서명이 조금 다릅니다.

import qualifield Data.Foldable as F

foldr :: (a -> b -> b) -> b -> [a] -> b

F.foldr :: F.Foldable t => (a -> b -> b) -> b -> t a -> b

Foldable의 foldr 함수가 좀 더 일반적인 형태를 갖고 있음을 알 수 있습니다.

Foldable

Foldable은 리스트 뿐만 아니라 접을 수 있는 모든 종류의 타입에 대해 foldl, foldr 등의 함수를

제공합니다. Foldable을 유용하게 활용하는 예제를 한 번 살펴봅시다.

data Tree a = Empty | Node a (Tree a) (Tree a)

deriving (Show, Read, Eq)

아주 간단한 이진 트리 데이터 타입입니다. 이 트리를 Foldable 타입 클래스에 속하게 만들면 트리의

전체 요소를 순회하며 어떤 연산을 수행하기 굉장히 쉬워집니다. Foldable 타입 클래스가 제공하는

함수는 굉장히 많지만 foldMap을 제외한 나머지 함수들은 foldMap을 이용한 기본 정의가 있기

때문에 이 한 가지 함수만 정의하면 됩니다. 그리고 foldMap 함수는 다음과 같은 타입을 갖고

있습니다.

foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

Foldable

foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

이 정의를 잘 보면 foldMap에서 적용할 함수는 a -> m 타입이라는 것을 알 수 있습니다. 즉, tree의

각 요소에 함수를 적용했을 때 결과는 모노이드에 속한 타입의 값이어야 한다는 거죠. 이 타입 서명을

이용해 앞서 정의한 Tree타입에 대한 foldMap 함수를 다음과 같이 정의할 수 있습니다.

instance F.Foldable Tree where

foldMap f Empty = mempty

foldMap f (Node x l r) = F.foldMap f l `mappend`

f x `mappend`

F.foldMap f r

Foldable

이렇게 정의한 foldMap 함수를 다음의 테스트용 Tree에 적용해봅시다.

testTree = Node 5

(Node 3

(Node 1 Empty Empty)

(Node 6 Empty Empty)

)

(Node 9

(Node 8 Empty Empty)

(Node 10 Empty Empty)

)

5

3 9

1 6 8 10

Foldable

Prelude> F.foldl (+) 0 testTree

42

Prelude> F.foldl (*) 1 testTree

64800

Prelude> F.foldMap (\x -> [x]) testTree

[1,3,6,5,8,9,10]

위와 같이 트리의 모든 원소를 더한 값, 곱한 값을

Foldable의 foldl 함수를 이용해 아주 쉽게 구할 수

있습니다.

또, 맨 아래 예제처럼 주어진 트리를 리스트

로 바꾸는 것 역시 아주 쉽게 할 수 있죠.

5

3 9

1 6 8 10