haskell study 15

21
Haskell Study 15. monad transformer

Upload: nam-hyeonuk

Post on 17-Jan-2017

446 views

Category:

Software


1 download

TRANSCRIPT

Page 1: Haskell study 15

Haskell Study

15. monad transformer

Page 2: Haskell study 15

Nested Monad

코드를 짜다보면 모나드 안에 모나드가 중첩되는 경우가 생각보다 빈번하게 발생합니다. 단순히

생각해봐도, 'stateful한 연산을 하면서 로그를 기록하고 싶다'라고 한다면 어떻게 해야할까요?

이건 State Monad와 Writer Monad를 동시에 사용해야만 하는 케이스입니다. 이런 경우에는

부득이하게 모나드가 중첩되어서 들어갈 수 밖에 없죠. 그리고 이렇게 모나드가 중첩되면 코드가

상당히 지저분해집니다. 이럴 때 좋은 해결책이 될 수 있는 것이 바로 Monad Transformer

입니다. 새로운 개념을 배울 때는 역시 예제부터 시작하는 것이 좋으니 이번에도 간단한 예제부터

시작해봅시다.

Page 3: Haskell study 15

Nested Monad

isValid "Red" = True

isValid "Blue" = True

isValid _ = False

check :: IO (Maybe String)

check = do

s <- getLine

return $ if isValid s

then Just s

else Nothing

문자열을 하나 입력받아 이게 정당한 조건을 만족한다면 Just s, 아니면 Nothing을 반환하는

함수입니다. 이 함수는 실패할 수도 있으면서 IO기도 하기 때문에 IO (Maybe String)타입을 가지죠.

Page 4: Haskell study 15

Nested Monad

twoCheck :: IO ()

twoCheck = do

c1 <- check

case c1 of

Just val -> do

c2 <- check

case c2 of

Just val2 ->

if val == val2

then putStrLn "Equal"

else putStrLn "Not Equal"

Nothing -> putStrLn "invalid Input"

Nothing -> putStrLn "invalid Input"

이 함수는 check 함수를 두 번 호출해서, 그 내부의 값에

따라 서로 다른 문자열을 화면에 출력하는 함수입니다. 예

전에 처음 Maybe 모나드에 대해 언급했을 때와 마찬가지

로, 실패할 수 있는 모든 케이스를 다 확인해야하기 때문에

코드가 굉장히 지저분합니다. 그래서 Maybe Monad를

이용하고 싶지만 모나드가 중첩되어 있어 적용하기가 쉽지

않죠. 이런 경우에 Monad Transformer를 사용할 수 있

습니다.

Page 5: Haskell study 15

MaybeT

Control.Monad.Trans.Maybe 모듈에는 MaybeT 라는 타입이 선언되어있습니다.

newtype MaybeT m a = MaybeT { runMaybeT m (Maybe a) }

MaybeT 자체는 단순히 newtype을 이용한 래핑이라는 것을 알 수 있습니다. 맨 안쪽에 Maybe

모나드가 들어가 있고, 그 모나드를 바깥에서 다른 모나드 m이 감싸고 있는 형태의 타입을

MaybeT m a로 정의한 거죠. 아까 저희가 정의한 함수 check의 타입은 IO (Maybe String)

이었는데, 이를 MaybeT 형태로 나타내면 MaybeT IO String이 됩니다. 이 자체로는 물론 아무런

의미가 없겠지만, 이 newtype에 대한 모나드 인스턴스가 정의되어있고 이를 이용해 중첩된 모나드를

마치 하나의 모나드인 것처럼 사용할 수 있게 됩니다.

Page 6: Haskell study 15

MaybeT

instance Monad m => Monad (MaybeT m) where

return = MaybeT . return . Just

x >>= f = MaybeT $ do

maybe_value <- runMaybeT x

case maybe_value of

Nothing -> return Nothing

Just value -> runMaybeT $ f value

Maybe T m에 대한 모나드 타입 클래스는 다음과 같이 정의되어있습니다. 이제 이 코드를 하나씩

따라가면서 Monad Transformer가 어떤 원리로 작동하는지 이해해봅시다.

Page 7: Haskell study 15

MaybeT

return = MaybeT . return . Just

return은 단순합니다. 주어진 값을 Just 값 생성자를 이용해 Maybe 값으로 만들고, 이걸 다시

return 함수를 통해 Maybe를 감싸고 있는 바깥 모나드 속으로 집어넣습니다. 이 시점에서 타입은

m (Maybe a) 가 되겠죠. 이걸 다시 MaybeT 값 생성자를 이용해 MaybeT 타입의 값으로 만듭니다.

외부 모나드로 한 번 감싸준다는 것을 제외하고는 Maybe 모나드의 원래 return 구현과 크게 다를 게

없죠.

Page 8: Haskell study 15

MaybeT

x >>= f = MaybeT $ do

maybe_value <- runMaybeT x

case maybe_value of

Nothing -> return Nothing

Just value -> runMaybeT $ f value

핵심인 >>= 함수입니다. 항상 타입을 기준으로 내용을 이해하면 쉽다고 말했죠. 여기서 >>= 함수의

타입은 MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b 입니다.

함수 구현부에서 do 표기법은 안쪽의 Maybe를 감싸고 있는 m 모나드에 대한 것입니다. 여기서

runMaybeT x를 통해 모나드 안쪽에 있는 Maybe 값을 가져오죠(maybe_value). 그리고 그 값이

Nothing이라면 return Nothing을 통해 Nothing값을 m 모나드로 감싸고, Just value라면 그

value 값을 f 함수에 넘겨서 나온 MaybeT 값을 돌려주죠. 역시, 외부를 m 모나드로 한 번 감싸는

것을 제외하곤 Maybe 함수의 구현과 별 다를 바가 없습니다.

Page 9: Haskell study 15

MaybeT

그리고 이에 부가적으로 중요한 MonadTrans 타입 클래스가 있습니다. 이 타입클래스는 Monad

Transformer와 관련된 타입클래스로, lift라는 함수를 제공합니다. lift 함수의 타입은 다음과

같습니다.

lift :: (MonadTrans t, Monad m) => m a -> t m a

이름처럼 그냥 모나드 값 (m a)을 모나드 트랜스포머(t) 내부로 들어올리는(lift) 함수입니다.

이 함수를 이용해서 외부에 있는 모나드 m과 관련된 함수를 따로 정의하거나 할 필요없이 모나드

트랜스포머 내부에서도 사용할 수 있게 됩니다.

Page 10: Haskell study 15

MaybeT

instance MonadTrans MaybeT where

lift = MaybeT . (liftM Just)

MaybeT 모나드 트랜스포머의 경우 lift 함수는 위와 같이 정의되어있습니다. 여기서 liftM

함수는 이전 슬라이드에서 다뤘듯이 단순히 fmap으로 생각하시면 됩니다. MaybeT의 경우 Maybe

외부의 함수 적용 결과는 내부로 들어올릴 때 단순히 Just만 적용해주면 문맥상 문제가 없겠죠

(Maybe 외부의 값을 들어올리는 것이므로 이 값은 Maybe의 문맥과는 상관이 없습니다. 따라서 Just

를 이용해 단순히 값만 Maybe 모나드 내부로 가져옵니다).

Page 11: Haskell study 15

MaybeT

이제 MaybeT를 이용해 앞서 짰던 코드를 다시 작성해봅시다. isValid 함수는 달라질 게 없으니 check

함수와 twoCheck 함수만 다시 작성해시다. 우선 check 함수입니다.

check :: MaybeT IO String

check = do

s <- lift getLine

guard (isValid s)

return s

Monad 파트에서 할 때 다뤘던 guard 함수를 이용해 조건에 맞지 않으면 MaybeT (IO Nothing)이

반환되고, 그 외의 경우에는 MaybeT (IO (Just s)) 값이 반환되게 만들었습니다.

Page 12: Haskell study 15

MaybeT

twoCheck :: MaybeT IO ()

twoCheck = do

c1 <- check

c2 <- check

if c1 == c2

then lift $ putStrLn "Equal"

else lift $ putStrLn "Not Equal"

twoCheck 함수는 정말 간결해졌습니다. IO와 Maybe 모나드가 동시에 사용되고 있지만, 제일 안

쪽에 있는 모나드인 Maybe 모나드만 쓰듯이 사용할 수 있게 됩니다. 물론 lift 함수를 이용해서 IO

모나드와 관련된 함수도 손쉽게 쓸 수 있죠.

Page 13: Haskell study 15

WriterT

이번엔 WriterT 모나드 트랜스포머에 대해 알아봅시다. 기본적으로 모나드 트랜스포머들은 기존의

모나드들을 다른 모나드들과 중첩시키기 위해 존재한다고 생각하시면 됩니다. 그리고 Writer, State

등등의 모나드의 경우 원래는 Writer와 WriterT 가 따로 존재했으나, 모나드 트랜스포머의 개념이

일반화되며 Writer는 단순히 아래와 같이 정의되게 변경되었습니다(State도 비슷합니다).

type Writer w = WriterT w Identity

Identity 모나드는 컨텍스트가 없는 모나드입니다(아무런 컨텍스트가 없는 기본 값에서의 연산과

동일하다고 생각하시면 됩니다). 즉, Writer 모나드는 WriterT 모나드 트랜스포머 중 Writer

모나드를 둘러 싼 바깥 모나드가 없는 경우(Identity)를 나타내는 것 뿐이라는 거죠. 그래서 WriterT

모나드 트랜스포머 값을 만들기 위해 writer 함수를 사용합니다.

writer :: Monad m => (a,w) -> WriterT w m a

Page 14: Haskell study 15

WriterT

그럼 이번엔 WriterT 모나드를 사용한 예제를 살펴봅시다. 이전엔 모나드 2개가 중첩되는 케이스를

살펴봤으니 이번엔 모나드 3개가 중첩되는 케이스를 보죠. 일단 여기서 하고 싶은 작업은 아래와

같습니다.

1. 숫자 2개를 입력받는다.

2. 그 숫자 2개를 곱한 결과를 구한다.

여기서, 숫자가 입력되었는지 아닌지를 확인하기 위해 Maybe, 그리고 입력을 위해 IO, 작업과정의

로깅을 위해 Writer를 사용할 겁니다.

Page 15: Haskell study 15

WriterT

우선 로그를 찍기 위한 logNumber 함수와 주어진 입력값이 정당한지 아닌지 판별할 때 쓸 isValid

함수의 구현부터 봅시다.

logNumber :: (Monad m) => Int -> WriterT [String] m Int

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

isValid :: String -> Bool

isValid = all (`elem` "0123456789")

isValid는 따로 설명이 필요없을 만큼 간단합니다. logNumber의 경우, 숫자 값 하나를 입력받아

WriterT [String] m Int 값을 반환합니다. 일반화된 로그 함수라고 생각할 수 있죠. logNumber

는 외부를 특정 모나드 m이 감싸고 있을 때, 그 내부 Writer 모나드에서 로그 작업을 수행하는

함수입니다.

Page 16: Haskell study 15

WriterT

readNum :: MaybeT (WriterT [String] IO) Int

readNum = do

str <- liftIO getLine

guard (isValid str)

let num = read str

lift $ logNumber num

return num

readNum 함수는 이전 슬라이드의 두 함수를 이용해 실제로 값을 입력받습니다. 제일 안쪽 모나드가

Maybe, 그 바깥이 Writer, 그 바깥이 IO 모나드의 형태가 되죠. liftIO 함수는 IO 작업 결과를 맨

안쪽 모나드로 끌어 올려주는 역할을 합니다. 나머지 동작은 여태껏 해왔던 것과 유사하죠. valid하지

않으면 Nothing, valid하면 값을 읽어서 기록하고 그 값을 돌려줍니다.

Page 17: Haskell study 15

WriterT

multiply :: MaybeT (WriterT [String] IO) Int

multiply = do

a <- readNum

b <- readNum

lift $ tell [show a ++ " * " ++ show b ++ " = " ++ show (a*b)]

return $ a*b

multiply 함수는 실제로 곱셈을 수행하는 함수입니다. 이 함수는 두 숫자를 입력받은 후 곱셈한

결과를 로깅하고, 그 결과값을 리턴해줍니다.

Page 18: Haskell study 15

WriterT

main = do

(num, log) <- runWriterT $ runMaybeT multiply

mapM putStrLn log

실제로 multiply 함수를 사용하는 예제입니다. main 함수에서 multiply 함수를 이용해 곱셈과

로깅을 수행하고, 그 값을 runMaybeT와 runWriterT를 통해 맨 바깥 IO 모나드까지 꺼내옵니다.

그리고 그 과정에서 얻은 로그 값을 mapM 함수를 이용해 화면에 출력합니다. mapM 함수는 map

함수의 모나드 버젼이라고 생각하시면 됩니다.

mapM :: Monad m => ( a -> m b ) -> [a] -> [m b]

Page 19: Haskell study 15

StateT

마지막으로 StateT 모나드에 대해서 살펴봅시다. StateT 모나드 역시 앞에서 다룬 것들과 크게

차이나지 않습니다. 이전에 구현했던 stack을 Writer 모나드를 이용해 push / pop할 때 로그까지

남기도록 아래와 같이 작성할 수 있습니다.

push :: (Show a, Monad m) => a -> StateT [a] (WriterT [String] m) ()

push val = StateT $ \s ->

writer (((), val:s), ["push" ++ show val])

타입이 조금 복잡해보이긴 하지만, 기존과 크게 다르진 않습니다. StateT 값 생성자의 타입은

StateT :: (s -> m (a, s)) -> StateT s m a 인데, stateful한 연산이 일어나는 State

모나드 바깥에 다른 모나드가 더 있다고 생각하면 왜 이런 타입을 가져야하는 지 이해하기 조금 쉬울

겁니다.

Page 20: Haskell study 15

StateT

pop :: (Show a, Monad m) => StateT [a] (WriterT [String] m) (Maybe a)

pop = StateT popFunc where

popFunc (x:xs) = writer ((Just x, xs), ["pop" ++ show x])

popFunc xs = writer ((Nothing, xs), ["stack is empty"])

pop은 스택이 비어있을 때를 다루기 위해 Maybe a 값을 리턴하게 만들어보았습니다. 스택이 비어

있다면 스택이 비었다는 로그를 남기며 Nothing을 돌려주고, 스택이 비지 않았다면 pop하면서 pop

한 원소가 무엇인지에 관한 로그를 같이 남겨주죠.

Page 21: Haskell study 15

StateT

stackIO :: StateT [String] (WriterT [String] IO) ()

stackIO = do

a <- liftIO getLine

b <- liftIO getLine

push a

push b

r1 <- pop

r2 <- pop

r3 <- pop

lift $ tell [show (r1,r2,r3)]

사용 예제입니다. 이전에 State 모나드에서 썼던 예제와 코드 구조가 크게 다르지 않습니다. 타입은 좀

많이 복잡해졌지만요..