haskell study 14
TRANSCRIPT
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 타입이 지닌 컨텍스트인 '비결정성'을 그대로 가지고 동작합니다.