haskell study 14

24
Haskell Study 14. useful monads

Upload: nam-hyeonuk

Post on 17-Jan-2017

340 views

Category:

Software


0 download

TRANSCRIPT

Haskell Study

14. useful monads

Writer Monad

이전에 다뤘던 Maybe, List, IO 등의 모나드 외의 많이 쓰이는 다른 유용한 모나드들에 대해

다뤄봅시다. 첫 번째는 Writer 모나드입니다. Writer 모나드는 작업 중간중간에 로그를 남기고 싶을

때 굉장히 유용하게 사용할 수 있습니다.

newtype Writer extra a = Writer { runWriter :: (a, extra) }

instance Monoid extra => Monad (Writer extra) where

return a = Writer (a, mempty)

Writer (a, e) >>= f =

let (b, e') = runWriter (f a) in Writer (b, e `mappend` e')

extra타입은 해당 작업의 결과로 인해 발생한 여분의 값(로그 등)의 타입이고 a 타입은 실제

작업하고자 하는 값의 타입입니다.

Writer Monad

Writer 모나드의 return 함수는 계산하고자 하는 값과, extra 타입의 가장 작은 값(mempty)을

묶은 튜플을 반환합니다.

Prelude> runWriter (return 3 :: Writer String Int)

(3, "")

Prelude> runWriter (return 3 :: Writer (Sum Int) Int)

(3, Sum {getSum = 0})

Prelude> runWriter (return 3 :: Writer (Product Int) Int)

(3, Product {getProduct = 1})

runWriter 함수는 Writer 타입의 값을 일반적인 튜플값으로 바꿔주는 역할을 하죠.

Writer Monad

Writer 모나드를 이용해 간단한 로그를 남기는 예제를 작성해봅시다.

import Control.Monad.Writer

logNumber :: Int -> Writer [String] Int

-- Writer가 아니라 writer인 것에 주의. 관련 내용은 Monad Transformer에서 다룹니다.

logNumber x = writer (x, ["Got Number: " ++ show x])

multWithLog :: Writer [String] Int

multWithLog = do

a <- logNumber 3

b <- logNumber 5

return (a*b)

Prelude> runWriter multWithLog

(15, ["Got Number: 3", "Got Number: 5"])

Writer Monad

앞의 예제와 같이 Writer 모나드를 사용하면 각각의 연산에 대한 결과 로그를 쉽게 남길 수 있다는

것을 알 수 있습니다. 그리고 Writer 모나드에서 유용하게 사용할 수 있는 함수로 tell이라는 함수가

있습니다.

tell :: extra -> Writer extra ()

tell e = writer ((), e)

tell 함수는 위 선언에서 볼 수 있듯이, 실제 연산 값에는 아무런 영향을 미치지 않고 여분의 값에 대해

특정 값을 추가하고 싶을 때 사용하는 함수입니다. 계산 중간중간에 원하는 로그를 삽입하고 싶을 때

사용할 수 있겠죠.

Writer Monad

tell을 활용하여, 두 수의 gcd를 구하는 함수의 연산 과정을 기록해봅시다.

import Control.Monad.Writer

gcdWithLog :: Int -> Int -> Writer [String] Int

gcdWithLog a b

| b == 0 = do

tell ["Finished with " ++ show a]

return a

| otherwise = do

tell [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)]

gcdWithLog b (a `mod` b)

Writer Monad

gcd 결과 값을 얻고 싶다면 함수를 수행한 Writer 값에서 첫 번째 요소를 가져오면 되고, 계산 로그가

궁금하다면 두 번째 요소를 가져오면 되겠죠.

Prelude> fst $ runWriter (gcdWithLog 8 3)

1

Prelude> snd $ runWriter (gcdWithLog 8 3)

["8 mod 3 = 2", "3 mod 2 = 1", "2 mod 1 = 0", "Finished With 1"]

State Monad

이번엔 State 모나드입니다. State 모나드는 상태의 변화가 필요한 연산을 구현할 때 굉장히

유용하게 쓸 수 있습니다.

newtype State s a = State { runState :: s -> (a,s) }

State s a는 s 타입의 상태와 a 타입의 결과값을 가지는 연산으로 생각할 수 있습니다.

instance Monad (State s) where

return x = State $ \s -> (x,s)

(State h) >>= f = State $ \s -> let (a, newState) = h s

(State g) = f a

in g newState

State Monad

return x = State $ \s -> (x,s)

코드가 어려울 땐 타입을 기준으로 하나씩 살펴보는 것이 좋습니다.

return 함수는 원래 (Monad m) => a -> m a라는 타입을 갖고 있는데, 이 때 m이 State s이므로

여기서 return 함수는 a -> State s a 라는 타입을 가지게 됩니다. 그래서 return x는 상태 s를

인자로 받아 결과값 x와 상태 s의 튜플을 돌려주는 연산으로 정의됩니다.

State Monad

(State h) >>= f = State $ \s -> let (a, newState) = h s

(State g) = f a

in g newState

State 모나드에서 >>= 함수는 두 개의 stateful한 연산을 이어주는 역할을 한다고 생각하면 됩니다.

역시 타입부터 하나씩 살펴봅시다.

원래 >>= 함수는 (Monad m) => m a -> (a -> m b) -> m b 라는 타입을 갖고 있습니다.

따라서 여기서는 타입이 State s a -> (a -> State s b) -> State s b가 되겠죠.

그리고 State s a의 내부에 저장된 값은 s -> (a,s)라는 타입을 가집니다. 이 타입을 머릿 속에

잘 새겨둔 상태에서 다음 슬라이드들을 따라가봅시다.

State Monad

(State h) >>= f = State $ \s -> let (a, newState) = h s

(State g) = f a

in g newState

우선 결과값은 State 값 생성자로 시작하고, 이 생성자에 람다를 인자로 주고 있습니다. 따라서 이

람다의 타입은 s -> (b,s)가 되어야겠죠.

State Monad

(State h) >>= f = State $ \s -> let (a, newState) = h s

(State g) = f a

in g newState

따라서 람다의 인자 s는 타입 s를 가지게 됩니다(타입 / 값 헷갈리면 안돼요!). 여기서 이제 let ~ in

구문이 나오죠. let 구문 안 쪽의 내용부터 봅시다.

State Monad

(State h) >>= f = State $ \s -> let (a, newState) = h s

(State g) = f a

in g newState

h s의 결과값을 (a, newState)로 나타내고 있습니다. h 함수는 처음에 말했듯이 s -> (a,s)

타입 서명을 갖고 있죠. 따라서 a값은 a 타입, newState값은 s 타입을 갖게 됩니다. 의미 상으로는

주어진 상태 s에 첫번째 stateful한 연산 h를 적용한 결과, 결과값 a와 바뀐 상태 newState를

얻었다고 할 수 있을 겁니다.

State Monad

(State h) >>= f = State $ \s -> let (a, newState) = h s

(State g) = f a

in g newState

이제 그 상태에서 f a의 결과값을 State g로 나타내고 있죠. f 함수는 a -> State s b 타입을

갖고 있으니, g 함수는 s -> (b,s) 타입을 갖게 될 겁니다. 의미적으로는, h 함수를 적용한

결과 얻은 a 값을 이용하는 두 번째 stateful한 연산 g를 f 함수를 이용해 구하는 것으로 생각할 수

있습니다.

State Monad

(State h) >>= f = State $ \s -> let (a, newState) = h s

(State g) = f a

in g newState

그리고 in 이후 구문에서 이렇게 얻은 새로운 stateful 연산 g에 newState를 넘기고 있죠. 그 결과는

g 함수의 타입이 s -> (b,s)이므로 (b,s) 타입이 될겁니다. 즉, 람다는s 타입의 값을 받아 (b,s)

타입을 리턴하고, 그러니 (State h) >>= f 의 최종 결과 타입은 State s b가 되겠죠.

따라서 (State h) >>= f 라는 식은 의미적으로는 stateful한 연산인 h 함수를 수행한 후, 그

결과값을 이용하는 stateful 연산 g에 h를 수행한 후의 현재 상태(newState)를 넘긴 후의 결과

(b,s), 즉 g 함수의 결과로 얻은 b타입의 값과 새로운 상태 s를 얻는 것으로 생각할 수 있죠. 결국 두

개의 stateful한 연산을 자연스럽게 하나로 엮어주게 되는 것입니다.

stack

State 모나드를 활용하는 예제로 stack를 생각해봅시다. stack은 push와 pop이라는 두 가지

연산을 지원합니다. push / pop은 현재의 stateful한 연산이므로 State 모나드를 이용해 구현할 수

있겠죠.

import Control.Monad.State

type Stack a = [a]

--역시 State가 아니라 state인 것에 주의.

push :: a -> State (Stack a) ()

push val = state $ \s -> ((), val:s)

pop :: State (Stack a) a

pop = state $ \(top:s) -> (top,s)

stack

이제 이렇게 구현한 stack을 한 번 테스트해봅시다.

stackTest :: State (Stack a) (a,a)

stackTest = do

a <- pop

push a

b <- pop

return (a,b)

ghci> runState stackTest [1,2,3]

((1,1),[2,3])

ghci> runState stackTest [5,4,3,2,1]

((5,5),(4,3,2,1))

Random

다른 유용한 State 모나드 활용 예제로는 random이 있습니다. System.Random 모듈에 있는 난수

생성 함수 random은 아래와 같은 타입을 갖고 있죠.

random :: (RandomGen g, Random a) => g -> (a, g)

이 함수는 난수의 시드값 역할을 할 수 있는 RandomGen 타입 클래스에 속하는 타입 g와, Random

타입 클래스에 속하는 타입 a에 대해 g 값을 받아 난수값 a와 그 이후 시드값 g의 튜플 (a,g)를

반환하는 함수입니다. 저 타입으로부터 random이 stateful 한 함수이며 따라서 State 모나드를

활용할 수 있다는 걸 알 수 있죠.

randomSt :: (RandomGen g, Random a) => State g a

randomSt = state random

Random

이제 randomSt 함수를 이용하면 여러 개의 random 값을 손쉽게 얻어낼 수 있습니다.

import System.Random

import Control.Monad.State

threeRandom :: State StdGen (Bool, Bool, Bool)

threeRandom = do

a <- randomSt

b <- randomSt

c <- randomSt

return (a,b,c)

ghci> runState threeRandom (mkStdGen 10)

((True, False, False),356856746 2103410263)

Useful functions

모나드를 쓸 때 유용한 함수 몇 가지를 살펴봅시다. 우선 liftM 함수와 ap 함수입니다.

liftM :: (Monad m) => (a -> b) -> m a -> m b

이 함수는 Monad에 대해 동작한다는 점만 다를 뿐 Functor의 fmap과 동일합니다. Monad가

Functor보다 더 나아간 개념이므로 Monad에 대해서도 fmap과 동일한 연산을 수행할 수 있는 것은

당연하겠죠.

ap :: (Monad m) => m (a -> b) -> m a -> m b

ap역시 Monad에 대해 동작한다는 점만 다를 뿐 Applicative Functor의 <*>과 동일합니다.

Useful functions

ghci> liftM (*2) (Just 5)

Just 10

ghci> liftM (*3) Nothing

Nothing

ghci> liftM (*5) [1,2,3]

[5,10,15]

ghci> Just (*2) `ap` Just 4

Just 8

ghci> Nothing `ap` Just 5

Nothing

ghci> [(*2),(+3)] `ap` [1,2,3]

[2,4,6,4,5,6]

Useful functions

다음은 filterM입니다. 고차함수를 다룰 때 나왔던 filter 함수가 list에 대해서만 동작하는 것이었다면,

filterM 함수는 그걸 일반적인 Monad 차원으로 확장시킨 함수입니다. 이 함수의 타입은 아래와

같습니다.

filter :: (a -> Bool) -> [a] -> [a]

filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]

이 filterM 함수를 사용하면 해당 monad의 컨텍스트와 관련된 filter 함수를 수행할 수 있습니다.

Useful functions

ghci> filterM (\_ -> Just True) [1,2,3]

Just [1,2,3]

ghci> filterM (\x -> if x == 2 then Just True else Just False) [1,2,3]

Just [2]

ghci> filterM (\_ -> Nothing) [1,2,3]

Nothing

위와 같이 Maybe 모나드에 대해 filterM 함수를 쓰면 원래 Maybe 모나드가 가진 컨텍스트인

'실패할 수 있는 연산'이 그대로 적용된다는 걸 알 수 있습니다. Nothing이 하나라도 포함되면

Nothing, 그렇지 않다면 술어함수를 통과한 값만 남기죠. 그렇다면 list 타입에 대한 filterM은 어떻게

동작할까요? 역시 마찬가지로 list 타입이 지닌 컨텍스트인 '비결정성'을 그대로 가지고 동작합니다.

Useful functions

이 비결정성이라는 특징을 이용해 어떤 집합의 멱집합(powerset - 해당 집합의 부분집합으로

이루어진 집합)을 쉽게 구할 수 있습니다.

powerset :: [a] -> [[a]]

powerset = filterM (\x -> [True, False])

ghci> powerset [1,2,3]

[[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]]