Haskell study 14

24
Haskell Study 14. useful monads

Transcript of Haskell study 14

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],[]]